From e2c509fe5370773116704c6f78d3f4c06e9b0b08 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 2 Apr 2024 16:06:20 +0200 Subject: [PATCH 01/36] feat: ts rewrite Fixed errors: - On dpt2 multiple subtypes 001: 010 011 012 - Ensure every file uses knxlog and not log-driver directly - Fix logs on dptlib/index.js (scalar and range not defined) - Varius dpt typos - DPT237 apdu_data[1] and [2] assigned to an array instead of a number --- index.js | 19 - package-lock.json | 1226 +++++++++++++---- package.json | 8 +- src/{Address.js => Address.ts} | 30 +- src/Connection.js | 368 ----- src/Connection.ts | 434 ++++++ src/Datapoint.js | 132 -- src/Datapoint.ts | 118 ++ src/FSM.js | 649 --------- src/FSM.ts | 666 +++++++++ ...ngConnection.js => IpRoutingConnection.ts} | 29 +- ...Connection.js => IpTunnelingConnection.ts} | 23 +- src/{KnxConstants.js => KnxConstants.ts} | 51 +- src/KnxLog.js | 28 - src/KnxLog.ts | 54 + src/{KnxProtocol.js => KnxProtocol.ts} | 339 ++--- .../{BinarySwitch.js => BinarySwitch.ts} | 37 +- src/devices/index.js | 1 - src/devices/index.ts | 5 + src/dptlib/dpt1.js | 223 --- src/dptlib/dpt1.ts | 235 ++++ src/dptlib/dpt10.js | 97 -- src/dptlib/dpt10.ts | 105 ++ src/dptlib/dpt11.js | 78 -- src/dptlib/dpt11.ts | 85 ++ src/dptlib/dpt12.js | 25 - src/dptlib/dpt12.ts | 31 + src/dptlib/dpt13.js | 73 - src/dptlib/dpt13.ts | 89 ++ src/dptlib/dpt14.js | 165 --- src/dptlib/dpt14.ts | 171 +++ src/dptlib/dpt15.js | 24 - src/dptlib/dpt15.ts | 33 + src/dptlib/dpt16.js | 45 - src/dptlib/dpt16.ts | 54 + src/dptlib/dpt17.js | 25 - src/dptlib/dpt17.ts | 29 + src/dptlib/dpt18.js | 84 -- src/dptlib/dpt18.ts | 96 ++ src/dptlib/dpt19.js | 56 - src/dptlib/dpt19.ts | 64 + src/dptlib/dpt2.js | 136 -- src/dptlib/dpt2.ts | 152 ++ src/dptlib/dpt20.js | 40 - src/dptlib/dpt20.ts | 49 + src/dptlib/dpt21.js | 78 -- src/dptlib/dpt21.ts | 87 ++ src/dptlib/dpt232.js | 51 - src/dptlib/dpt232.ts | 58 + src/dptlib/dpt237.js | 72 - src/dptlib/dpt237.ts | 84 ++ src/dptlib/dpt238.js | 74 - src/dptlib/dpt238.ts | 82 ++ src/dptlib/dpt3.js | 78 -- src/dptlib/dpt3.ts | 85 ++ src/dptlib/dpt4.js | 49 - src/dptlib/dpt4.ts | 55 + src/dptlib/dpt5.js | 54 - src/dptlib/dpt5.ts | 70 + src/dptlib/dpt6.js | 33 - src/dptlib/dpt6.ts | 43 + src/dptlib/dpt7.js | 101 -- src/dptlib/dpt7.ts | 120 ++ src/dptlib/dpt8.js | 83 -- src/dptlib/dpt8.ts | 90 ++ src/dptlib/dpt9.js | 229 --- src/dptlib/dpt9.ts | 237 ++++ src/dptlib/index.js | 203 --- src/dptlib/index.ts | 244 ++++ src/index.ts | 19 + src/types/binary-protocol.d.ts | 10 + src/types/machina.d.ts | 40 + tsconfig.build.json | 5 + tsconfig.json | 34 + 74 files changed, 5049 insertions(+), 3900 deletions(-) delete mode 100644 index.js rename src/{Address.js => Address.ts} (91%) delete mode 100644 src/Connection.js create mode 100644 src/Connection.ts delete mode 100644 src/Datapoint.js create mode 100644 src/Datapoint.ts delete mode 100644 src/FSM.js create mode 100644 src/FSM.ts rename src/{IpRoutingConnection.js => IpRoutingConnection.ts} (66%) rename src/{IpTunnelingConnection.js => IpTunnelingConnection.ts} (52%) rename src/{KnxConstants.js => KnxConstants.ts} (53%) delete mode 100644 src/KnxLog.js create mode 100644 src/KnxLog.ts rename src/{KnxProtocol.js => KnxProtocol.ts} (73%) rename src/devices/{BinarySwitch.js => BinarySwitch.ts} (68%) delete mode 100644 src/devices/index.js create mode 100644 src/devices/index.ts delete mode 100644 src/dptlib/dpt1.js create mode 100644 src/dptlib/dpt1.ts delete mode 100644 src/dptlib/dpt10.js create mode 100644 src/dptlib/dpt10.ts delete mode 100644 src/dptlib/dpt11.js create mode 100644 src/dptlib/dpt11.ts delete mode 100644 src/dptlib/dpt12.js create mode 100644 src/dptlib/dpt12.ts delete mode 100644 src/dptlib/dpt13.js create mode 100644 src/dptlib/dpt13.ts delete mode 100644 src/dptlib/dpt14.js create mode 100644 src/dptlib/dpt14.ts delete mode 100644 src/dptlib/dpt15.js create mode 100644 src/dptlib/dpt15.ts delete mode 100644 src/dptlib/dpt16.js create mode 100644 src/dptlib/dpt16.ts delete mode 100644 src/dptlib/dpt17.js create mode 100644 src/dptlib/dpt17.ts delete mode 100644 src/dptlib/dpt18.js create mode 100644 src/dptlib/dpt18.ts delete mode 100644 src/dptlib/dpt19.js create mode 100644 src/dptlib/dpt19.ts delete mode 100644 src/dptlib/dpt2.js create mode 100644 src/dptlib/dpt2.ts delete mode 100644 src/dptlib/dpt20.js create mode 100644 src/dptlib/dpt20.ts delete mode 100644 src/dptlib/dpt21.js create mode 100644 src/dptlib/dpt21.ts delete mode 100644 src/dptlib/dpt232.js create mode 100644 src/dptlib/dpt232.ts delete mode 100644 src/dptlib/dpt237.js create mode 100644 src/dptlib/dpt237.ts delete mode 100644 src/dptlib/dpt238.js create mode 100644 src/dptlib/dpt238.ts delete mode 100644 src/dptlib/dpt3.js create mode 100644 src/dptlib/dpt3.ts delete mode 100644 src/dptlib/dpt4.js create mode 100644 src/dptlib/dpt4.ts delete mode 100644 src/dptlib/dpt5.js create mode 100644 src/dptlib/dpt5.ts delete mode 100644 src/dptlib/dpt6.js create mode 100644 src/dptlib/dpt6.ts delete mode 100644 src/dptlib/dpt7.js create mode 100644 src/dptlib/dpt7.ts delete mode 100644 src/dptlib/dpt8.js create mode 100644 src/dptlib/dpt8.ts delete mode 100644 src/dptlib/dpt9.js create mode 100644 src/dptlib/dpt9.ts delete mode 100644 src/dptlib/index.js create mode 100644 src/dptlib/index.ts create mode 100644 src/index.ts create mode 100644 src/types/binary-protocol.d.ts create mode 100644 src/types/machina.d.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/index.js b/index.js deleted file mode 100644 index 7fcc8cc..0000000 --- a/index.js +++ /dev/null @@ -1,19 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2017 Elias Karakoulakis -*/ - -const path = require('path'); -const util = require('util'); -const log = require('log-driver').logger; - -const knx_path = path.join(__dirname, 'package.json'); -const pkginfo = require(knx_path); - -log.info(util.format('Loading %s: %s, version: %s', - pkginfo.name, pkginfo.description, pkginfo.version)); - -exports.Connection = require('./src/Connection.js'); -exports.Datapoint = require('./src/Datapoint.js'); -exports.Devices = require('./src/devices'); -exports.Log = require('./src/KnxLog.js'); diff --git a/package-lock.json b/package-lock.json index b95daaa..c05848a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,588 +1,1272 @@ { "name": "knx", - "version": "2.3.7", - "lockfileVersion": 1, + "version": "2.5.4", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@types/node": { - "version": "6.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.13.tgz", - "integrity": "sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw==" + "packages": { + "": { + "name": "knx", + "version": "2.5.4", + "license": "MIT", + "dependencies": { + "binary-parser": "^2.2.1", + "binary-protocol": "0.0.0", + "ipaddr.js": "1.2.0", + "log-driver": "1.2.7", + "machina": "^4.0.2" + }, + "devDependencies": { + "@types/node": "^20.12.2", + "esbuild-register": "^3.5.0", + "multi-tape": "^1.2.1", + "tape": "^4.10.1", + "typescript": "^5.4.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } }, - "balanced-match": { + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/node": { + "version": "20.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz", + "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "binary-parser": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-1.1.5.tgz", - "integrity": "sha1-ciTWGk4vp7Wu3mn+Tw6moJAreeg=" + "node_modules/binary-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", + "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==", + "engines": { + "node": ">=12" + } }, - "binary-protocol": { + "node_modules/binary-protocol": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/binary-protocol/-/binary-protocol-0.0.0.tgz", "integrity": "sha1-ZU4tbCB8HS7qpl1dais+ulmu024=", - "requires": { - "bluebird": "2.2.2" + "dependencies": { + "bluebird": "~2.2.2" } }, - "bluebird": { + "node_modules/bluebird": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.2.2.tgz", "integrity": "sha1-8b8Fq8iHz5pwOIYjfChhCkOx8RQ=" }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "requires": { - "balanced-match": "1.0.0", + "dependencies": { + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "call-bind": { + "node_modules/call-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", "dev": true, - "requires": { - "function-bind": "1.1.1", - "get-intrinsic": "1.0.1" + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "deep-equal": { + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", "dev": true, - "requires": { - "is-arguments": "1.0.4", - "is-date-object": "1.0.2", - "is-regex": "1.0.5", - "object-is": "1.1.3", - "object-keys": "1.1.1", - "regexp.prototype.flags": "1.3.0" + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "define-properties": { + "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, - "requires": { - "object-keys": "1.1.1" + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" } }, - "defined": { + "node_modules/defined": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", "dev": true }, - "dotignore": { + "node_modules/dotignore": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", "dev": true, - "requires": { - "minimatch": "3.0.4" + "dependencies": { + "minimatch": "^3.0.4" + }, + "bin": { + "ignored": "bin/ignored" } }, - "es-abstract": { + "node_modules/es-abstract": { "version": "1.18.0-next.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", "dev": true, - "requires": { - "es-to-primitive": "1.2.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "has-symbols": "1.0.1", - "is-callable": "1.2.2", - "is-negative-zero": "2.0.0", - "is-regex": "1.1.1", - "object-inspect": "1.8.0", - "object-keys": "1.1.1", - "object.assign": "4.1.2", - "string.prototype.trimend": "1.0.2", - "string.prototype.trimstart": "1.0.2" - }, - "dependencies": { - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "dev": true, - "requires": { - "has-symbols": "1.0.1" - } - }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", - "dev": true - } + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "es-to-primitive": { + "node_modules/es-abstract/node_modules/object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, - "requires": { - "is-callable": "1.2.2", - "is-date-object": "1.0.2", - "is-symbol": "1.0.3" + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/esbuild-register": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", + "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" } }, - "events-to-array": { + "node_modules/events-to-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", "dev": true }, - "for-each": { + "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, - "requires": { - "is-callable": "1.2.2" + "dependencies": { + "is-callable": "^1.1.3" } }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "function-bind": { + "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "get-intrinsic": { + "node_modules/get-intrinsic": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", "dev": true, - "requires": { - "function-bind": "1.1.1", - "has": "1.0.3", - "has-symbols": "1.0.1" + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "glob": { + "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "has": { + "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, - "requires": { - "function-bind": "1.1.1" + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" } }, - "has-symbols": { + "node_modules/has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "ipaddr.js": { + "node_modules/ipaddr.js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.2.0.tgz", - "integrity": "sha1-irpJyRknmVhb3WQ+DMtQ6K53e6Q=" + "integrity": "sha1-irpJyRknmVhb3WQ+DMtQ6K53e6Q=", + "engines": { + "node": ">= 0.10" + } }, - "is-arguments": { + "node_modules/is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.4" + } }, - "is-callable": { + "node_modules/is-callable": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-date-object": { + "node_modules/is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-negative-zero": { + "node_modules/is-negative-zero": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.4" + } }, - "is-regex": { + "node_modules/is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "dev": true, - "requires": { - "has": "1.0.3" + "dependencies": { + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-symbol": { + "node_modules/is-symbol": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, - "requires": { - "has-symbols": "1.0.1" + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lodash": { + "node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, - "log-driver": { + "node_modules/log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==" + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "engines": { + "node": ">=0.8.6" + } }, - "machina": { + "node_modules/machina": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/machina/-/machina-4.0.2.tgz", "integrity": "sha512-OOlFrW1rd783S6tF36v5Ie/TM64gfvSl9kYLWL2cPA31J71HHWW3XrgSe1BZSFAPkh8532CMJMLv/s9L2aopiA==", - "requires": { - "lodash": "4.17.20" + "dependencies": { + "lodash": "^4.17.5" + }, + "engines": { + "node": ">=0.4.0" } }, - "minimatch": { + "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "requires": { - "brace-expansion": "1.1.11" + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "minimist": { + "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "minipass": { + "node_modules/minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "dev": true, - "requires": { - "safe-buffer": "5.2.1", - "yallist": "3.1.1" + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" } }, - "multi-tape": { + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/multi-tape": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/multi-tape/-/multi-tape-1.4.0.tgz", "integrity": "sha512-uPnzBRNyJdWgC4N8ta2r0gwQMgRY69v0M2JNAwTu3uzfdjYCeLQXDAfjYpcR1agZo+czuCQNbeCTi0+IVswklA==", "dev": true, - "requires": { - "glob": "7.1.6", - "minimist": "1.2.5", - "stream-buffers": "3.0.2", - "tap-parser": "9.3.3", - "tee": "0.2.0" + "dependencies": { + "glob": "^7.1.4", + "minimist": "^1.2.0", + "stream-buffers": "^3.0.2", + "tap-parser": "^9.3.2", + "tee": "^0.2.0" + }, + "bin": { + "multi-tape": "build/index.js" } }, - "object-inspect": { + "node_modules/object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "object-is": { + "node_modules/object-is": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", "dev": true, - "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.18.0-next.1" + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "object-keys": { + "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.4" + } }, - "object.assign": { + "node_modules/object.assign": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, - "requires": { - "call-bind": "1.0.0", - "define-properties": "1.1.3", - "has-symbols": "1.0.1", - "object-keys": "1.1.1" + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "requires": { - "wrappy": "1.0.2" + "dependencies": { + "wrappy": "1" } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "path-parse": { + "node_modules/path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, - "regexp.prototype.flags": { + "node_modules/regexp.prototype.flags": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", "dev": true, - "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.7" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "dev": true, - "requires": { - "es-to-primitive": "1.2.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "has-symbols": "1.0.1", - "is-callable": "1.2.2", - "is-regex": "1.1.1", - "object-inspect": "1.8.0", - "object-keys": "1.1.1", - "object.assign": "4.1.2", - "string.prototype.trimend": "1.0.2", - "string.prototype.trimstart": "1.0.2" - } - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "dev": true, - "requires": { - "has-symbols": "1.0.1" - } - }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", - "dev": true - } + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags/node_modules/es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "resolve": { + "node_modules/regexp.prototype.flags/node_modules/is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags/node_modules/object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "dev": true, - "requires": { - "path-parse": "1.0.6" + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "resumer": { + "node_modules/resumer": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", "dev": true, - "requires": { - "through": "2.3.8" - }, "dependencies": { - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - } + "through": "~2.3.4" } }, - "safe-buffer": { + "node_modules/resumer/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "stream-buffers": { + "node_modules/stream-buffers": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.10.0" + } }, - "string.prototype.trim": { + "node_modules/string.prototype.trim": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz", "integrity": "sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==", "dev": true, - "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.18.0-next.1" + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "string.prototype.trimend": { + "node_modules/string.prototype.trimend": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", "dev": true, - "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.18.0-next.1" + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "string.prototype.trimstart": { + "node_modules/string.prototype.trimstart": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "dev": true, - "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.18.0-next.1" + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "tap-parser": { + "node_modules/tap-parser": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-9.3.3.tgz", "integrity": "sha512-VlC7tlSZ3EGt2qPLSa9CTJepNkc2yCh7uzhzAF5DxnuujeKbFbKxMA+fxtTWEN2j/KgfGi+mgooiZPKkOhEQyw==", "dev": true, - "requires": { - "events-to-array": "1.1.2", - "minipass": "2.9.0", - "tap-yaml": "1.0.0" + "dependencies": { + "events-to-array": "^1.0.1", + "minipass": "^2.2.0", + "tap-yaml": "^1.0.0" + }, + "bin": { + "tap-parser": "bin/cmd.js" } }, - "tap-yaml": { + "node_modules/tap-yaml": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-1.0.0.tgz", "integrity": "sha512-Rxbx4EnrWkYk0/ztcm5u3/VznbyFJpyXO12dDBHKWiDVxy7O2Qw6MRrwO5H6Ww0U5YhRY/4C/VzWmFPhBQc4qQ==", "dev": true, - "requires": { - "yaml": "1.10.0" + "dependencies": { + "yaml": "^1.5.0" } }, - "tape": { + "node_modules/tape": { "version": "4.13.3", "resolved": "https://registry.npmjs.org/tape/-/tape-4.13.3.tgz", "integrity": "sha512-0/Y20PwRIUkQcTCSi4AASs+OANZZwqPKaipGCEwp10dQMipVvSZwUUCi01Y/OklIGyHKFhIcjock+DKnBfLAFw==", "dev": true, - "requires": { - "deep-equal": "1.1.1", - "defined": "1.0.0", - "dotignore": "0.1.2", - "for-each": "0.3.3", - "function-bind": "1.1.1", - "glob": "7.1.6", - "has": "1.0.3", - "inherits": "2.0.4", - "is-regex": "1.0.5", - "minimist": "1.2.5", - "object-inspect": "1.7.0", - "resolve": "1.17.0", - "resumer": "0.0.0", - "string.prototype.trim": "1.2.2", - "through": "2.3.8" - }, - "dependencies": { - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - } + "dependencies": { + "deep-equal": "~1.1.1", + "defined": "~1.0.0", + "dotignore": "~0.1.2", + "for-each": "~0.3.3", + "function-bind": "~1.1.1", + "glob": "~7.1.6", + "has": "~1.0.3", + "inherits": "~2.0.4", + "is-regex": "~1.0.5", + "minimist": "~1.2.5", + "object-inspect": "~1.7.0", + "resolve": "~1.17.0", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.2.1", + "through": "~2.3.8" + }, + "bin": { + "tape": "bin/tape" } }, - "tee": { + "node_modules/tape/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/tee": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/tee/-/tee-0.2.0.tgz", "integrity": "sha1-2HtYIHoMmb1icXnCwsYjXj051yU=", "dev": true, - "requires": { - "through": "1.1.2" + "dependencies": { + "through": "~1.1.1" } }, - "through": { + "node_modules/through": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/through/-/through-1.1.2.tgz", "integrity": "sha1-NEpUJaN3MxTKfg62US+6+vdsC/4=", "dev": true }, - "wrappy": { + "node_modules/typescript": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "yallist": { + "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "yaml": { + "node_modules/yaml": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 6" + } } } } diff --git a/package.json b/package.json index e84a87e..ea87eef 100644 --- a/package.json +++ b/package.json @@ -88,15 +88,17 @@ "building automation" ], "dependencies": { - "@types/node": "^6.14.4", - "binary-parser": "1.1.5", + "binary-parser": "^2.2.1", "binary-protocol": "0.0.0", "ipaddr.js": "1.2.0", "log-driver": "1.2.7", "machina": "^4.0.2" }, "devDependencies": { + "@types/node": "^20.12.2", + "esbuild-register": "^3.5.0", "multi-tape": "^1.2.1", - "tape": "^4.10.1" + "tape": "^4.10.1", + "typescript": "^5.4.3" } } diff --git a/src/Address.js b/src/Address.ts similarity index 91% rename from src/Address.js rename to src/Address.ts index 38ccfe4..921be8b 100644 --- a/src/Address.js +++ b/src/Address.ts @@ -2,9 +2,13 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -const KnxLog = require('./KnxLog'); -const Parser = require('binary-parser').Parser; +import KnxLog from './KnxLog'; +import { Parser } from 'binary-parser'; +interface AddressType { + PHYSICAL: number; + GROUP: number; +} // +-----------------------------------------------+ // 16 bits | INDIVIDUAL ADDRESS | // +-----------------------+-----------------------+ @@ -37,7 +41,7 @@ const Parser = require('binary-parser').Parser; // +--+--------------------+-----------------------+ // NOTE: ets4 can utilise all 5 bits for the main group (0..31) -const TYPE = { +const TYPE: AddressType = { PHYSICAL: 0x00, GROUP: 0x01, }; @@ -48,10 +52,10 @@ const twoLevel = new Parser().bit5('l1').bit11('l2'); // convert address stored in two-byte buffer to string const toString = function ( - buf /*buffer*/, - addrtype /*ADDRESS_TYPE*/, - twoLevelAddressing /*boolean*/ -) { + buf: string | Buffer, + addrtype: number, + twoLevelAddressing = false +): string { const group = addrtype == TYPE.GROUP; //KnxLog.get().trace('%j, type: %d, %j', buf, addrtype, knxnetprotocol.twoLevelAddressing); if (!Buffer.isBuffer(buf) || buf.length !== 2) @@ -69,13 +73,13 @@ const toString = function ( }; // check for out of range integer -const r = (x, max) => x < 0 || x > max; +const r = (x: number, max: number): boolean => x < 0 || x > max; // parse address string to 2-byte Buffer const parse = function ( - addr /*string*/, - addrtype /*TYPE*/, - twoLevelAddressing -) { + addr: string, + addrtype: number, + twoLevelAddressing = false +): Buffer { if (!addr) { KnxLog.get().warn('Fix your code - no address given to Address.parse'); } @@ -113,4 +117,4 @@ const parse = function ( return address; }; -module.exports = { TYPE, toString, parse }; +export default { TYPE, toString, parse }; \ No newline at end of file diff --git a/src/Connection.js b/src/Connection.js deleted file mode 100644 index 71e72b1..0000000 --- a/src/Connection.js +++ /dev/null @@ -1,368 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -const util = require('util'); - -const FSM = require('./FSM'); -const DPTLib = require('./dptlib'); -const KnxLog = require('./KnxLog'); -const KnxConstants = require('./KnxConstants'); -const KnxNetProtocol = require('./KnxProtocol'); - -// bind incoming UDP packet handler -FSM.prototype.onUdpSocketMessage = function(msg, rinfo, callback) { - // get the incoming packet's service type ... - try { - const reader = KnxNetProtocol.createReader(msg); - reader.KNXNetHeader('tmp'); - const dg = reader.next()['tmp']; - const descr = datagramDesc(dg); - KnxLog.get().trace('(%s): Received %s message: %j', this.compositeState(), descr, dg); - if (!isNaN(this.channel_id) && - ((dg.hasOwnProperty('connstate') && - dg.connstate.channel_id != this.channel_id) || - (dg.hasOwnProperty('tunnstate') && - dg.tunnstate.channel_id != this.channel_id))) { - KnxLog.get().trace('(%s): *** Ignoring %s datagram for other channel (own: %d)', - this.compositeState(), descr, this.channel_id); - } else { - // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") - const signal = util.format('inbound_%s', descr); - if (descr === "DISCONNECT_REQUEST") { - KnxLog.get().info("empty internal fsm queue due to %s: ", signal); - this.clearQueue(); - } - this.handle(signal, dg); - } - } catch(err) { - KnxLog.get().debug('(%s): Incomplete/unparseable UDP packet: %s: %s', - this.compositeState(),err, msg.toString('hex') - ); - } -}; - -FSM.prototype.AddConnState = function(datagram) { - datagram.connstate = { - channel_id: this.channel_id, - state: 0 - } -} - -FSM.prototype.AddTunnState = function(datagram) { - // add the remote IP router's endpoint - datagram.tunnstate = { - channel_id: this.channel_id, - tunnel_endpoint: this.remoteEndpoint.addr + ':' + this.remoteEndpoint.port - } -} - -const AddCRI = (datagram) => { - // add the CRI - datagram.cri = { - connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, - knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, - unused: 0 - } -} - -FSM.prototype.AddCEMI = function(datagram, msgcode) { - const sendAck = ((msgcode || 0x11) == 0x11) && !this.options.suppress_ack_ldatareq; // only for L_Data.req - datagram.cemi = { - msgcode: msgcode || 0x11, // default: L_Data.req for tunneling - ctrl: { - frameType: 1, // 0=extended 1=standard - reserved: 0, // always 0 - repeat: 1, // the OPPOSITE: 1=do NOT repeat - broadcast: 1, // 0-system broadcast 1-broadcast - priority: 3, // 0-system 1-normal 2-urgent 3-low - acknowledge: sendAck ? 1 : 0, - confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error - // 2nd byte - destAddrType: 1, // FIXME: 0-physical 1-groupaddr - hopCount: 6, - extendedFrame: 0 - }, - src_addr: this.options.physAddr || "15.15.15", - dest_addr: "0/0/0", // - apdu: { - // default operation is GroupValue_Write - apci: 'GroupValue_Write', - tpci: 0, - data: 0 - } - } -} - -/* - * submit an outbound request to the state machine - * - * type: service type - * datagram_template: - * if a datagram is passed, use this as - * if a function is passed, use this to DECORATE - * if NULL, then just make a new empty datagram. Look at AddXXX methods - */ -FSM.prototype.Request = function(type, datagram_template, callback) { - // populate skeleton datagram - const datagram = this.prepareDatagram(type); - // decorate the datagram, if a function is passed - if (typeof datagram_template == 'function') { - datagram_template(datagram); - } - // make sure that we override the datagram service type! - datagram.service_type = type; - const st = KnxConstants.keyText('SERVICE_TYPE', type); - // hand off the outbound request to the state machine - this.handle('outbound_' + st, datagram); - if (typeof callback === 'function') callback(); -} - -// prepare a datagram for the given service type -FSM.prototype.prepareDatagram = function(svcType) { - const datagram = { - "header_length": 6, - "protocol_version": 16, // 0x10 == version 1.0 - "service_type": svcType, - "total_length": null, // filled in automatically - } - // - AddHPAI(datagram); - // - switch (svcType) { - case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: - AddTunn(datagram); - AddCRI(datagram); // no break! - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: - case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: - this.AddConnState(datagram); - break; - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - this.AddCEMI(datagram, KnxConstants.MESSAGECODES['L_Data.ind']); - break; - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - AddTunn(datagram); - this.AddTunnState(datagram); - this.AddCEMI(datagram); - break; - case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - this.AddTunnState(datagram); - break; - default: - KnxLog.get().debug('Do not know how to deal with svc type %d', svcType); - } - return datagram; -} - -/* -send the datagram over the wire -*/ -FSM.prototype.send = function(datagram, callback) { - var cemitype; // TODO: set, but unused - try { - this.writer = KnxNetProtocol.createWriter(); - switch (datagram.service_type) { - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - // append the CEMI service type if this is a tunneling request... - cemitype = KnxConstants.keyText('MESSAGECODES', datagram.cemi.msgcode); - break; - } - const packet = this.writer.KNXNetHeader(datagram); - const buf = packet.buffer; - const svctype = KnxConstants.keyText('SERVICE_TYPE', datagram.service_type); // TODO: unused - const descr = datagramDesc(datagram); - KnxLog.get().trace('(%s): Sending %s ==> %j', this.compositeState(), descr, datagram); - this.socket.send( - buf, 0, buf.length, - this.remoteEndpoint.port, this.remoteEndpoint.addr.toString(), - (err) => { - KnxLog.get().trace('(%s): UDP sent %s: %s %s', this.compositeState(), - (err ? err.toString() : 'OK'), descr, buf.toString('hex') - ); - if (typeof callback === 'function') callback(err); - } - ); - } catch (e) { - KnxLog.get().warn(e); - if (typeof callback === 'function') callback(e); - } -} - -FSM.prototype.write = function(grpaddr, value, dptid, callback) { - if (grpaddr == null || value == null) { - KnxLog.get().warn('You must supply both grpaddr and value!'); - return; - } - try { - // outbound request onto the state machine - const serviceType = this.useTunneling ? - KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : - KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function(datagram) { - DPTLib.populateAPDU(value, datagram.cemi.apdu, dptid); - datagram.cemi.dest_addr = grpaddr; - }, callback); - } catch (e) { - KnxLog.get().warn(e); - } -} - -FSM.prototype.respond = function(grpaddr, value, dptid) { - if (grpaddr == null || value == null) { - KnxLog.get().warn('You must supply both grpaddr and value!'); - return; - } - const serviceType = this.useTunneling ? - KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : - KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function(datagram) { - DPTLib.populateAPDU(value, datagram.cemi.apdu, dptid); - // this is a READ request - datagram.cemi.apdu.apci = "GroupValue_Response"; - datagram.cemi.dest_addr = grpaddr; - return datagram; - }); -} - -FSM.prototype.writeRaw = function(grpaddr, value, bitlength, callback) { - if (grpaddr == null || value == null) { - KnxLog.get().warn('You must supply both grpaddr and value!'); - return; - } - if (!Buffer.isBuffer(value)) { - KnxLog.get().warn('Value must be a buffer!'); - return; - } - // outbound request onto the state machine - const serviceType = this.useTunneling ? - KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : - KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function(datagram) { - datagram.cemi.apdu.data = value; - datagram.cemi.apdu.bitlength = bitlength ? bitlength : (value.byteLength * 8); - datagram.cemi.dest_addr = grpaddr; - }, callback); -} - -// send a READ request to the bus -// you can pass a callback function which gets bound to the RESPONSE datagram event -FSM.prototype.read = function(grpaddr, callback) { - if (typeof callback == 'function') { - // when the response arrives: - const responseEvent = 'GroupValue_Response_' + grpaddr; - KnxLog.get().trace('Binding connection to ' + responseEvent); - const binding = (src, data) => { - // unbind the event handler - this.off(responseEvent, binding); - // fire the callback - callback(src, data); - } - // prepare for the response - this.on(responseEvent, binding); - // clean up after 3 seconds just in case no one answers the read request - setTimeout( () => this.off(responseEvent, binding), 3000); - } - const serviceType = this.useTunneling ? - KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : - KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function(datagram) { - // this is a READ request - datagram.cemi.apdu.apci = "GroupValue_Read"; - datagram.cemi.dest_addr = grpaddr; - return datagram; - }); -} - -FSM.prototype.Disconnect = function(cb) { - var that = this; - - if(this.state === 'connecting') { - KnxLog.get().debug('Disconnecting directly'); - that.transition("uninitialized"); - if(cb) { - cb() - } - return - } - - KnxLog.get().debug('waiting for Idle-State'); - this.onIdle(function() { - KnxLog.get().trace('In Idle-State'); - - that.on('disconnected', () => { - KnxLog.get().debug('Disconnected from KNX'); - if(cb) { - cb() - } - }); - - KnxLog.get().debug('Disconnecting from KNX'); - that.transition("disconnecting"); - }); - - // machina.js removeAllListeners equivalent: - // this.off(); -} - -FSM.prototype.onIdle = function(cb) { - if(this.state === 'idle') { - KnxLog.get().trace('Connection is already Idle'); - cb(); - } - else { - this.on("transition", function(data) { - if(data.toState === 'idle') { - KnxLog.get().trace('Connection just transitioned to Idle'); - cb(); - } - }); - } -} - -// return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) -const datagramDesc = (dg) => { - let blurb = KnxConstants.keyText('SERVICE_TYPE', dg.service_type); - if (dg.service_type == KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || - dg.service_type == KnxConstants.SERVICE_TYPE.ROUTING_INDICATION) { - blurb += '_' + KnxConstants.keyText('MESSAGECODES', dg.cemi.msgcode); - } - return blurb; -} - -// add the control udp local endpoint. UPDATE: not needed apparnently? -const AddHPAI = (datagram) => { - datagram.hpai = { - protocol_type: 1, // UDP - //tunnel_endpoint: this.localAddress + ":" + this.control.address().port - tunnel_endpoint: '0.0.0.0:0' - }; -} - -// add the tunneling udp local endpoint UPDATE: not needed apparently? -const AddTunn = (datagram) => { - datagram.tunn = { - protocol_type: 1, // UDP - tunnel_endpoint: '0.0.0.0:0' - //tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port - }; -} - -// TODO: Conncetion is obviously not a constructor, but tests call it with `new`. That should be deprecated. -function Connection(options) { - const conn = new FSM(options); - // register with the FSM any event handlers passed into the options object - if (typeof options.handlers === 'object') { - for (const [key, value] of Object.entries(options.handlers)) { - if (typeof value === 'function') { - conn.on(key, value); - } - } - } - // boot up the KNX connection unless told otherwise - if (!options.manualConnect) conn.Connect(); - return conn; -}; - -module.exports = Connection; diff --git a/src/Connection.ts b/src/Connection.ts new file mode 100644 index 0000000..e98d444 --- /dev/null +++ b/src/Connection.ts @@ -0,0 +1,434 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import * as util from "util"; + +import KnxFSM, { Datagram } from "./FSM"; +import { populateAPDU } from "./dptlib"; +import KnxLog from "./KnxLog"; +import { keyText, KnxConstants } from "./KnxConstants"; +import KnxNetProtocol from "./KnxProtocol"; + +// bind incoming UDP packet handler +KnxFSM.prototype.onUdpSocketMessage = function ( + msg: Buffer, + rinfo: any, + callback: () => void +): void { + // get the incoming packet's service type ... + try { + const reader = KnxNetProtocol.createReader(msg); + reader.KNXNetHeader("tmp"); + const dg = reader.next()["tmp"]; + const descr = datagramDesc(dg); + KnxLog.get().trace( + "(%s): Received %s message: %j", + this.compositeState(), + descr, + dg + ); + if ( + !isNaN(this.channel_id) && + ((dg.hasOwnProperty("connstate") && + dg.connstate.channel_id != this.channel_id) || + (dg.hasOwnProperty("tunnstate") && + dg.tunnstate.channel_id != this.channel_id)) + ) { + KnxLog.get().trace( + "(%s): *** Ignoring %s datagram for other channel (own: %d)", + this.compositeState(), + descr, + this.channel_id + ); + } else { + // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") + const signal = util.format("inbound_%s", descr); + if (descr === "DISCONNECT_REQUEST") { + KnxLog.get().info("empty internal fsm queue due to %s: ", signal); + this.clearQueue(); + } + this.handle(signal, dg); + } + } catch (err) { + KnxLog.get().debug( + "(%s): Incomplete/unparseable UDP packet: %s: %s", + this.compositeState(), + err, + msg.toString("hex") + ); + } +}; + +KnxFSM.prototype.AddConnState = function (datagram: Datagram): void { + datagram.connstate = { + channel_id: this.channel_id, + state: 0, + }; +}; + +KnxFSM.prototype.AddTunnState = function (datagram: Datagram): void { + // add the remote IP router's endpoint + datagram.tunnstate = { + channel_id: this.channel_id, + tunnel_endpoint: this.remoteEndpoint.addr + ":" + this.remoteEndpoint.port, + }; +}; + +const AddCRI = (datagram: Datagram): void => { + // add the CRI + datagram.cri = { + connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, + knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, + unused: 0, + }; +}; + +KnxFSM.prototype.AddCEMI = function (datagram: Datagram, msgcode: number): void { + const sendAck = + (msgcode || 0x11) == 0x11 && !this.options.suppress_ack_ldatareq; // only for L_Data.req + datagram.cemi = { + msgcode: msgcode || 0x11, // default: L_Data.req for tunneling + ctrl: { + frameType: 1, // 0=extended 1=standard + reserved: 0, // always 0 + repeat: 1, // the OPPOSITE: 1=do NOT repeat + broadcast: 1, // 0-system broadcast 1-broadcast + priority: 3, // 0-system 1-normal 2-urgent 3-low + acknowledge: sendAck ? 1 : 0, + confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error + // 2nd byte + destAddrType: 1, // FIXME: 0-physical 1-groupaddr + hopCount: 6, + extendedFrame: 0, + }, + src_addr: this.options.physAddr || "15.15.15", + dest_addr: "0/0/0", // + apdu: { + // default operation is GroupValue_Write + apci: "GroupValue_Write", + tpci: 0, + data: 0, + }, + }; +}; + +/* + * submit an outbound request to the state machine + * + * type: service type + * datagram_template: + * if a datagram is passed, use this as + * if a function is passed, use this to DECORATE + * if NULL, then just make a new empty datagram. Look at AddXXX methods + */ +KnxFSM.prototype.Request = function ( + type: number, + datagram_template: (datagram: Datagram) => void, + callback: () => void +): void { + // populate skeleton datagram + const datagram = this.prepareDatagram(type); + // decorate the datagram, if a function is passed + if (typeof datagram_template == "function") { + datagram_template(datagram); + } + // make sure that we override the datagram service type! + datagram.service_type = type; + const st = keyText("SERVICE_TYPE", type); + // hand off the outbound request to the state machine + this.handle("outbound_" + st, datagram); + if (typeof callback === "function") callback(); +}; + +// prepare a datagram for the given service type +KnxFSM.prototype.prepareDatagram = function (svcType: number): Datagram { + const datagram: Datagram = { + header_length: 6, + protocol_version: 16, // 0x10 == version 1.0 + service_type: svcType, + total_length: null, // filled in automatically + }; + // + AddHPAI(datagram); + // + switch (svcType) { + case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: + AddTunn(datagram); + AddCRI(datagram); // no break! + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: + case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: + this.AddConnState(datagram); + break; + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + this.AddCEMI(datagram, KnxConstants.MESSAGECODES["L_Data.ind"]); + break; + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + AddTunn(datagram); + this.AddTunnState(datagram); + this.AddCEMI(datagram); + break; + case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: + this.AddTunnState(datagram); + break; + default: + KnxLog.get().debug("Do not know how to deal with svc type %d", svcType); + } + return datagram; +}; + +/* +send the datagram over the wire +*/ +KnxFSM.prototype.send = function ( + datagram: Datagram, + callback: (err?: Error) => void +): void { + let cemitype: string; // TODO: set, but unused + try { + this.writer = KnxNetProtocol.createWriter(); + switch (datagram.service_type) { + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + // append the CEMI service type if this is a tunneling request... + cemitype = keyText("MESSAGECODES", datagram.cemi.msgcode); + break; + } + const packet = this.writer.KNXNetHeader(datagram); + const buf = packet.buffer; + const svctype = keyText("SERVICE_TYPE", datagram.service_type); // TODO: unused + const descr = datagramDesc(datagram); + KnxLog.get().trace( + "(%s): Sending %s ==> %j", + this.compositeState(), + descr, + datagram + ); + this.socket.send( + buf, + 0, + buf.length, + this.remoteEndpoint.port, + this.remoteEndpoint.addr.toString(), + (err) => { + KnxLog.get().trace( + "(%s): UDP sent %s: %s %s", + this.compositeState(), + err ? err.toString() : "OK", + descr, + buf.toString("hex") + ); + if (typeof callback === "function") callback(err); + } + ); + } catch (e) { + KnxLog.get().warn(e); + if (typeof callback === "function") callback(e); + } +}; + +KnxFSM.prototype.write = function ( + grpaddr: string, + value: any, + dptid: number, + callback: () => void +): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn("You must supply both grpaddr and value!"); + return; + } + try { + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request( + serviceType, + function (datagram: Datagram) { + populateAPDU(value, datagram.cemi.apdu, dptid); + datagram.cemi.dest_addr = grpaddr; + }, + callback + ); + } catch (e) { + KnxLog.get().warn(e); + } +}; + +KnxFSM.prototype.respond = function ( + grpaddr: string, + value: any, + dptid: number +): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn("You must supply both grpaddr and value!"); + return; + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request(serviceType, function (datagram: Datagram) { + populateAPDU(value, datagram.cemi.apdu, dptid); + // this is a READ request + datagram.cemi.apdu.apci = "GroupValue_Response"; + datagram.cemi.dest_addr = grpaddr; + return datagram; + }); +}; + +KnxFSM.prototype.writeRaw = function ( + grpaddr: string, + value: Buffer, + bitlength: number, + callback: () => void +): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn("You must supply both grpaddr and value!"); + return; + } + if (!Buffer.isBuffer(value)) { + KnxLog.get().warn("Value must be a buffer!"); + return; + } + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request( + serviceType, + function (datagram: Datagram) { + datagram.cemi.apdu.data = value; + datagram.cemi.apdu.bitlength = bitlength + ? bitlength + : value.byteLength * 8; + datagram.cemi.dest_addr = grpaddr; + }, + callback + ); +}; + +// send a READ request to the bus +// you can pass a callback function which gets bound to the RESPONSE datagram event +KnxFSM.prototype.read = function ( + grpaddr: string, + callback: (src: any, data: any) => void +): void { + if (typeof callback == "function") { + // when the response arrives: + const responseEvent = "GroupValue_Response_" + grpaddr; + KnxLog.get().trace("Binding connection to " + responseEvent); + const binding = (src: any, data: any) => { + // unbind the event handler + this.off(responseEvent, binding); + // fire the callback + callback(src, data); + }; + // prepare for the response + this.on(responseEvent, binding); + // clean up after 3 seconds just in case no one answers the read request + setTimeout(() => this.off(responseEvent, binding), 3000); + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request(serviceType, function (datagram: Datagram) { + // this is a READ request + datagram.cemi.apdu.apci = "GroupValue_Read"; + datagram.cemi.dest_addr = grpaddr; + return datagram; + }); +}; + +KnxFSM.prototype.Disconnect = function (cb: () => void): void { + var that = this; + + if (this.state === "connecting") { + KnxLog.get().debug("Disconnecting directly"); + that.transition("uninitialized"); + if (cb) { + cb(); + } + return; + } + + KnxLog.get().debug("waiting for Idle-State"); + this.onIdle(function () { + KnxLog.get().trace("In Idle-State"); + + that.on("disconnected", () => { + KnxLog.get().debug("Disconnected from KNX"); + if (cb) { + cb(); + } + }); + + KnxLog.get().debug("Disconnecting from KNX"); + that.transition("disconnecting"); + }); + + // machina.js removeAllListeners equivalent: + // this.off(); +}; + +KnxFSM.prototype.onIdle = function (cb: () => void): void { + if (this.state === "idle") { + KnxLog.get().trace("Connection is already Idle"); + cb(); + } else { + this.on("transition", function (data: any) { + if (data.toState === "idle") { + KnxLog.get().trace("Connection just transitioned to Idle"); + cb(); + } + }); + } +}; + +// return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) +const datagramDesc = (dg: Datagram): string => { + let blurb = keyText("SERVICE_TYPE", dg.service_type); + if ( + dg.service_type == KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || + dg.service_type == KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + ) { + blurb += "_" + keyText("MESSAGECODES", dg.cemi.msgcode); + } + return blurb; +}; + +// add the control udp local endpoint. UPDATE: not needed apparnently? +const AddHPAI = (datagram: Datagram): void => { + datagram.hpai = { + protocol_type: 1, // UDP + //tunnel_endpoint: this.localAddress + ":" + this.control.address().port + tunnel_endpoint: "0.0.0.0:0", + }; +}; + +// add the tunneling udp local endpoint UPDATE: not needed apparently? +const AddTunn = (datagram: Datagram): void => { + datagram.tunn = { + protocol_type: 1, // UDP + tunnel_endpoint: "0.0.0.0:0", + //tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port + }; +}; + +// TODO: Conncetion is obviously not a constructor, but tests call it with `new`. That should be deprecated. +function Connection(options: any): any { + const conn = new KnxFSM(options); + // register with the FSM any event handlers passed into the options object + if (typeof options.handlers === "object") { + for (const [key, value] of Object.entries(options.handlers)) { + if (typeof value === "function") { + conn.on(key, value); + } + } + } + // boot up the KNX connection unless told otherwise + if (!options.manualConnect) conn.Connect(); + return conn; +} + +export default Connection; diff --git a/src/Datapoint.js b/src/Datapoint.js deleted file mode 100644 index fa49df7..0000000 --- a/src/Datapoint.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const util = require('util'); -const DPTLib = require('./dptlib'); -const KnxLog = require('./KnxLog'); -const { EventEmitter } = require('events'); - -/* - * A Datapoint is always bound to: - * - a group address (eg. '1/2/3') - * - (optionally) a datapoint type (defaults to DPT1.001) - * You can also supply a valid connection to skip calling bind() - */ -class Datapoint extends EventEmitter { - constructor(options, conn) { - if (options == null || options.ga == null) - throw 'must supply at least { ga, dpt }!'; - super(); - - this.options = options; - this.dptid = options.dpt || 'DPT1.001'; - this.dpt = DPTLib.resolve(this.dptid); - KnxLog.get().trace('resolved %s to %j', this.dptid, this.dpt); - this.current_value = null; - if (conn) this.bind(conn); - } - - /* - * Bind the datapoint to a bus connection - */ - bind(conn) { - if (!conn) throw 'must supply a valid KNX connection to bind to'; - this.conn = conn; - // bind generic event handler for our group address - const gaevent = util.format('event_%s', this.options.ga); - conn.on(gaevent, (evt, src, buf) => { - // get the Javascript value from the raw buffer, if the DPT defines fromBuffer() - switch (evt) { - case 'GroupValue_Write': - case 'GroupValue_Response': - if (buf) { - const jsvalue = DPTLib.fromBuffer(buf, this.dpt); - this.emit('event', evt, jsvalue); - this.update(jsvalue); // update internal state - } - break; - default: - this.emit('event', evt); - // TODO: add default handler; maybe emit warning? - } - }); - // issue a GroupValue_Read request to try to get the initial state from the bus (if any) - if (this.options.autoread) - if (conn.conntime) { - // immediately or... - this.read(); - } else { - // ... when the connection is established - conn.on('connected', () => { - this.read(); - }); - } - } - - update(jsvalue) { - const old_value = this.current_value; - if (old_value === jsvalue) return; - - this.emit('change', this.current_value, jsvalue, this.options.ga); - this.current_value = jsvalue; // TODO: This should probably change before the event is emitted - const ts = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); // TODO: Why are we timestamping, the logger formatter already timestamps. - KnxLog.get().trace( - '%s **** %s DATAPOINT CHANGE (was: %j)', - ts, - this.toString(), - old_value - ); - } - - /* format a Javascript value into the APDU format dictated by the DPT - and submit a GroupValue_Write to the connection */ - write(value) { - if (!this.conn) throw 'must supply a valid KNX connection to bind to'; - if (this.dpt.hasOwnProperty('range')) { - // check if value is in range - const { range } = this.dpt.basetype; - const [min, max] = range; - if (value < min || value > max) { - throw util.format( - 'Value %j(%s) out of bounds(%j) for %s', - value, - typeof value, - range, - this.dptid - ); - } - } - this.conn.write( - this.options.ga, - value, - this.dptid, - // once we've written to the bus, update internal state - () => this.update(value) - ); - } - - /* - * Issue a GroupValue_Read request to the bus for this datapoint - * use the optional callback() to get notified upon response - */ - read(callback) { - if (!this.conn) throw 'must supply a valid KNX connection to bind to'; - this.conn.read(this.options.ga, (src, buf) => { - const jsvalue = DPTLib.fromBuffer(buf, this.dpt); - if (typeof callback == 'function') callback(src, jsvalue); - }); - } - - toString() { - return util.format( - '(%s) %s %s', - this.options.ga, - this.current_value, - (this.dpt.subtype && this.dpt.subtype.unit) || '' - ); - } -} - -module.exports = Datapoint; diff --git a/src/Datapoint.ts b/src/Datapoint.ts new file mode 100644 index 0000000..8afb417 --- /dev/null +++ b/src/Datapoint.ts @@ -0,0 +1,118 @@ +import { EventEmitter } from 'events'; +import { format } from 'util'; +import * as DPTLib from './dptlib'; +import KnxLog from './KnxLog'; + +interface Options { + ga: string; + dpt?: string; + autoread?: boolean; +} + +class Datapoint extends EventEmitter { + private options: Options; + private dptid: string; + private dpt: any; + private current_value: any; + private conn: any; + + constructor(options: Options, conn: any) { + if (options == null || options.ga == null) + throw new Error('must supply at least { ga, dpt }!'); + super(); + + this.options = options; + this.dptid = options.dpt || 'DPT1.001'; + this.dpt = DPTLib.resolve(this.dptid); + KnxLog.get().trace('resolved %s to %j', this.dptid, this.dpt); + this.current_value = null; + if (conn) this.bind(conn); + } + + bind(conn: any) { + if (!conn) throw new Error('must supply a valid KNX connection to bind to'); + this.conn = conn; + const gaevent = format('event_%s', this.options.ga); + conn.on(gaevent, (evt: string, src: any, buf: any) => { + switch (evt) { + case 'GroupValue_Write': + case 'GroupValue_Response': + if (buf) { + const jsvalue = DPTLib.fromBuffer(buf, this.dpt); + this.emit('event', evt, jsvalue); + this.update(jsvalue); + } + break; + default: + this.emit('event', evt); + } + }); + if (this.options.autoread) + if (conn.conntime) { + this.read(); + } else { + conn.on('connected', () => { + this.read(); + }); + } + } + + update(jsvalue: any) { + const old_value = this.current_value; + if (old_value === jsvalue) return; + + this.emit('change', this.current_value, jsvalue, this.options.ga); + this.current_value = jsvalue; + const ts = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); + KnxLog.get().trace( + '%s **** %s DATAPOINT CHANGE (was: %j)', + ts, + this.toString(), + old_value + ); + } + + write(value: any) { + if (!this.conn) throw new Error('must supply a valid KNX connection to bind to'); + if (this.dpt.hasOwnProperty('range')) { + const { range } = this.dpt.basetype; + const [min, max] = range; + if (value < min || value > max) { + throw new Error( + format( + 'Value %j(%s) out of bounds(%j) for %s', + value, + typeof value, + range, + this.dptid + ) + ); + } + } + this.conn.write( + this.options.ga, + value, + this.dptid, + () => this.update(value) + ); + } + + read(callback?: (src: any, jsvalue: any) => void) { + if (!this.conn) throw new Error('must supply a valid KNX connection to bind to'); + this.conn.read(this.options.ga, (src: any, buf: any) => { + const jsvalue = DPTLib.fromBuffer(buf, this.dpt); + if (typeof callback == 'function') callback(src, jsvalue); + }); + } + + toString() { + return format( + '(%s) %s %s', + this.options.ga, + this.current_value, + (this.dpt.subtype && this.dpt.subtype.unit) || '' + ); + } +} + +export default Datapoint; \ No newline at end of file diff --git a/src/FSM.js b/src/FSM.js deleted file mode 100644 index 15d8b52..0000000 --- a/src/FSM.js +++ /dev/null @@ -1,649 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const os = require('os'); -const util = require('util'); -const ipaddr = require('ipaddr.js'); -const machina = require('machina'); -const KnxConstants = require('./KnxConstants.js'); -const IpRoutingConnection = require('./IpRoutingConnection.js'); -const IpTunnelingConnection = require('./IpTunnelingConnection.js'); -const KnxLog = require('./KnxLog.js'); - -module.exports = machina.Fsm.extend({ - initialize(options) { - this.options = options || {}; - // initialise the log driver - to set the loglevel - this.log = KnxLog.get(options); - // set the local IP endpoint - this.localAddress = null; - this.ThreeLevelGroupAddressing = true; - // reconnection cycle counter - this.reconnection_cycles = 0; - // a cache of recently sent requests - this.sentTunnRequests = {}; - this.useTunneling = options.forceTunneling || false; - this.remoteEndpoint = { - addrstring: options.ipAddr || '224.0.23.12', - addr: ipaddr.parse(options.ipAddr || '224.0.23.12'), - port: options.ipPort || 3671, - }; - const range = this.remoteEndpoint.addr.range(); - this.localEchoInTunneling = - typeof options.localEchoInTunneling !== 'undefined' - ? options.localEchoInTunneling - : false; // 14=73/2020 Supergiovane (local echo of emitEvent if in tunneling mode) - this.log.debug( - 'initializing %s connection to %s', - range, - this.remoteEndpoint.addrstring - ); - switch (range) { - case 'multicast': - if (this.localEchoInTunneling) { - this.localEchoInTunneling = false; - this.log.debug( - 'localEchoInTunneling: true but DISABLED because i am on multicast' - ); - } // 14/03/2020 Supergiovane: if multicast, disable the localEchoInTunneling, because there is already an echo - IpRoutingConnection(this); - break; - case 'unicast': - case 'private': - case 'loopback': - this.useTunneling = true; - IpTunnelingConnection(this); - break; - default: - throw util.format( - 'IP address % (%s) cannot be used for KNX', - options.ipAddr, - range - ); - } - }, - - namespace: 'knxnet', - - initialState: 'uninitialized', - - states: { - uninitialized: { - ['*']() { - this.transition('connecting'); - }, - }, - - jumptoconnecting: { - _onEnter() { - this.transition('connecting'); - }, - }, - - connecting: { - _onEnter() { - // tell listeners that we disconnected - // putting this here will result in a correct state for our listeners - this.emit('disconnected'); - this.log.debug(util.format('useTunneling=%j', this.useTunneling)); - if (this.useTunneling) { - let connection_attempts = 0; - if (!this.localAddress) - throw 'Not bound to an IPv4 non-loopback interface'; - this.log.debug( - util.format('Connecting via %s...', this.localAddress) - ); - // we retry 3 times, then restart the whole cycle using a slower and slower rate (max delay is 5 minutes) - this.connecttimer = setInterval(() => { - connection_attempts += 1; - if (connection_attempts >= 3) { - clearInterval(this.connecttimer); - // quite a few KNXnet/IP devices drop any tunneling packets received via multicast - if (this.remoteEndpoint.addr.range() == 'multicast') { - this.log.warn( - 'connection timed out, falling back to pure routing mode...' - ); - this.usingMulticastTunneling = true; - this.transition('connected'); - } else { - // we restart the connection cycle with a growing delay (max 5 minutes) - this.reconnection_cycles += 1; - const delay = Math.min(this.reconnection_cycles * 3, 300); - this.log.debug( - 'reattempting connection in ' + delay + ' seconds' - ); - setTimeout( - // restart connecting cycle (cannot jump straight to 'connecting' so we use an intermediate state) - () => this.transition('jumptoconnecting'), - delay * 1000 - ); - } - } else { - this.log.warn('connection timed out, retrying...'); - this.send( - this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) - ); - } - }, 3000); - delete this.channel_id; - delete this.conntime; - delete this.lastSentTime; - // send connect request directly - this.send( - this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) - ); - } else { - // no connection sequence needed in pure multicast routing - this.transition('connected'); - } - }, - _onExit() { - clearInterval(this.connecttimer); - }, - inbound_CONNECT_RESPONSE(datagram) { - this.log.debug(util.format('got connect response')); - if ( - datagram.hasOwnProperty('connstate') && - datagram.connstate.status === - KnxConstants.RESPONSECODE.E_NO_MORE_CONNECTIONS - ) { - try { - this.socket.close(); - } catch (error) {} - this.transition('uninitialized'); - this.emit('disconnected'); - this.log.debug( - 'The KNXnet/IP server rejected the data connection (Maximum connections reached). Waiting 1 minute before retrying...' - ); - setTimeout(() => { - this.Connect(); - }, 60000); - } else { - // store channel ID into the Connection object - this.channel_id = datagram.connstate.channel_id; - // send connectionstate request directly - this.send( - this.prepareDatagram( - KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST - ) - ); - // TODO: handle send err - } - }, - inbound_CONNECTIONSTATE_RESPONSE(datagram) { - if (this.useTunneling) { - const str = KnxConstants.keyText( - 'RESPONSECODE', - datagram.connstate.status - ); - this.log.debug( - util.format( - 'Got connection state response, connstate: %s, channel ID: %d', - str, - datagram.connstate.channel_id - ) - ); - this.transition('connected'); - } - }, - ['*'](data) { - this.log.debug(util.format('*** deferring Until Transition %j', data)); - this.deferUntilTransition('idle'); - }, - }, - - connected: { - _onEnter() { - // Reset connection reattempts cycle counter for next disconnect - this.reconnection_cycles = 0; - // Reset outgoing sequence counter.. - this.seqnum = -1; - /* important note: the sequence counter is SEPARATE for incoming and - outgoing datagrams. We only keep track of the OUTGOING L_Data.req - and we simply acknowledge the incoming datagrams with their own seqnum */ - this.lastSentTime = this.conntime = Date.now(); - this.log.debug( - util.format( - '--- Connected in %s mode ---', - this.useTunneling ? 'TUNNELING' : 'ROUTING' - ) - ); - this.transition('idle'); - this.emit('connected'); - }, - }, - - disconnecting: { - // TODO: skip on pure routing - _onEnter() { - if (this.useTunneling) { - const aliveFor = this.conntime ? Date.now() - this.conntime : 0; - KnxLog.get().debug( - '(%s):\tconnection alive for %d seconds', - this.compositeState(), - aliveFor / 1000 - ); - this.disconnecttimer = setTimeout(() => { - KnxLog.get().debug( - '(%s):\tconnection timed out', - this.compositeState() - ); - try { - this.socket.close(); - } catch (error) {} - this.transition('uninitialized'); - this.emit('disconnected'); - }, 3000); - // - this.send( - this.prepareDatagram(KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST), - (err) => { - // TODO: handle send err - KnxLog.get().debug( - '(%s):\tsent DISCONNECT_REQUEST', - this.compositeState() - ); - } - ); - } - }, - _onExit() { - clearTimeout(this.disconnecttimer); - }, - inbound_DISCONNECT_RESPONSE(datagram) { - if (this.useTunneling) { - KnxLog.get().debug( - '(%s):\tgot disconnect response', - this.compositeState() - ); - try { - this.socket.close(); - } catch (error) {} - this.transition('uninitialized'); - this.emit('disconnected'); - } - }, - }, - - idle: { - _onEnter() { - if (this.useTunneling) { - if (this.idletimer == null) { // set one - // time out on inactivity... - this.idletimer = setTimeout( () => { - this.transition('requestingConnState'); - clearTimeout(this.idletimer); - this.idletimer = null; - }, 60000); - } - } - // debuglog the current FSM state plus a custom message - KnxLog.get().debug('(%s):\t%s', this.compositeState(), ' zzzz...'); - // process any deferred items from the FSM internal queue - this.processQueue(); - }, - _onExit() { - //clearTimeout(this.idletimer); - }, - // while idle we can either... - - // 1) queue an OUTGOING routing indication... - outbound_ROUTING_INDICATION(datagram) { - const elapsed = Date.now() - this.lastSentTime; - // if no miminum delay set OR the last sent datagram was long ago... - if ( - !this.options.minimumDelay || - elapsed >= this.options.minimumDelay - ) { - // ... send now - this.transition('sendDatagram', datagram); - } else { - // .. or else, let the FSM handle it later - setTimeout( - () => this.handle('outbound_ROUTING_INDICATION', datagram), - this.minimumDelay - elapsed - ); - } - }, - - // 2) queue an OUTGOING tunelling request... - outbound_TUNNELING_REQUEST(datagram) { - if (this.useTunneling) { - const elapsed = Date.now() - this.lastSentTime; - // if no miminum delay set OR the last sent datagram was long ago... - if ( - !this.options.minimumDelay || - elapsed >= this.options.minimumDelay - ) { - // ... send now - this.transition('sendDatagram', datagram); - } else { - // .. or else, let the FSM handle it later - setTimeout( - () => this.handle('outbound_TUNNELING_REQUEST', datagram), - this.minimumDelay - elapsed - ); - } - } else { - KnxLog.get().debug( - "(%s):\tdropping outbound TUNNELING_REQUEST, we're in routing mode", - this.compositeState() - ); - } - }, - - // 3) receive an INBOUND tunneling request INDICATION (L_Data.ind) - ['inbound_TUNNELING_REQUEST_L_Data.ind'](datagram) { - if (this.useTunneling) { - this.transition('recvTunnReqIndication', datagram); - } - }, - - /* 4) receive an INBOUND tunneling request CONFIRMATION (L_Data.con) to one of our sent tunnreq's - * We don't need to explicitly wait for a L_Data.con confirmation that the datagram has in fact - * reached its intended destination. This usually requires setting the 'Sending' flag - * in ETS, usually on the 'primary' device that contains the actuator endpoint - */ - ['inbound_TUNNELING_REQUEST_L_Data.con'](datagram) { - if (this.useTunneling) { - const confirmed = this.sentTunnRequests[datagram.cemi.dest_addr]; - if (confirmed) { - delete this.sentTunnRequests[datagram.cemi.dest_addr]; - this.emit('confirmed', confirmed); - } - KnxLog.get().trace( - '(%s): %s %s', - this.compositeState(), - datagram.cemi.dest_addr, - confirmed - ? 'delivery confirmation (L_Data.con) received' - : 'unknown dest addr' - ); - this.acknowledge(datagram); - } - }, - - // 5) receive an INBOUND ROUTING_INDICATION (L_Data.ind) - ['inbound_ROUTING_INDICATION_L_Data.ind'](datagram) { - this.emitEvent(datagram); - }, - - inbound_DISCONNECT_REQUEST(datagram) { - if (this.useTunneling) { - this.transition('connecting'); - } - }, - }, - - // if idle for too long, request connection state from the KNX IP router - requestingConnState: { - _onEnter() { - // added to note sending connectionstate_request - KnxLog.get().debug( 'Requesting Connection State'); - KnxLog.get().trace( - '(%s): Requesting Connection State', - this.compositeState() - ); - this.send( - this.prepareDatagram( - KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST - ) - ); - // TODO: handle send err - // - this.connstatetimer = setTimeout(() => { - const msg = 'timed out waiting for CONNECTIONSTATE_RESPONSE'; - KnxLog.get().trace('(%s): %s', this.compositeState(), msg); - this.transition('connecting'); - this.emit('error', msg); - }, 1000); - }, - _onExit() { - clearTimeout(this.connstatetimer); - }, - inbound_CONNECTIONSTATE_RESPONSE(datagram) { - const state = KnxConstants.keyText( - 'RESPONSECODE', - datagram.connstate.status - ); - switch (datagram.connstate.status) { - case 0: - this.transition('idle'); - break; - default: - this.log.debug( - util.format( - '*** error: %s *** (connstate.code: %d)', - state, - datagram.connstate.status - ) - ); - this.transition('connecting'); - this.emit('error', state); - } - }, - ['*'](data) { - this.log.debug( - util.format( - '*** deferring %s until transition from requestingConnState => idle', - data.inputType - ) - ); - this.deferUntilTransition('idle'); - }, - }, - - /* - * 1) OUTBOUND DATAGRAM (ROUTING_INDICATION or TUNNELING_REQUEST) - */ - sendDatagram: { - _onEnter(datagram) { - // send the telegram on the wire - this.seqnum += 1; - if (this.useTunneling) datagram.tunnstate.seqnum = this.seqnum & 0xff; - this.send(datagram, (err) => { - if (err) { - //console.trace('error sending datagram, going idle'); - this.seqnum -= 1; - this.transition('idle'); - } else { - // successfully sent the datagram - if (this.useTunneling) - this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; - this.lastSentTime = Date.now(); - this.log.debug( - '(%s):\t>>>>>>> successfully sent seqnum: %d', - this.compositeState(), - this.seqnum - ); - if (this.useTunneling) { - // and then wait for the acknowledgement - this.transition('sendTunnReq_waitACK', datagram); - } else { - this.transition('idle'); - } - } - // 14/03/2020 Supergiovane: In multicast mode, other node-red nodes receives the echo of the telegram sent (the groupaddress_write event). If in tunneling, force the emit of the echo datagram (so other node-red nodes can receive the echo), because in tunneling, there is no echo. - // ######################## - //if (this.useTunneling) this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; - if (this.useTunneling) { - this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; - if ( - typeof this.localEchoInTunneling !== 'undefined' && - this.localEchoInTunneling - ) { - try { - this.emitEvent(datagram); - this.log.debug( - '(%s):\t>>>>>>> localEchoInTunneling: echoing by emitting %d', - this.compositeState(), - this.seqnum - ); - } catch (error) { - this.log.debug( - '(%s):\t>>>>>>> localEchoInTunneling: error echoing by emitting %d ' + - error, - this.compositeState(), - this.seqnum - ); - } - } - } - // ######################## - }); - }, - ['*'](data) { - this.log.debug( - util.format( - '*** deferring %s until transition sendDatagram => idle', - data.inputType - ) - ); - this.deferUntilTransition('idle'); - }, - }, - /* - * Wait for tunneling acknowledgement by the IP router; this means the sent UDP packet - * reached the IP router and NOT that the datagram reached its final destination - */ - sendTunnReq_waitACK: { - _onEnter(datagram) { - //this.log.debug('setting up tunnreq timeout for %j', datagram); - this.tunnelingAckTimer = setTimeout(() => { - this.log.debug('timed out waiting for TUNNELING_ACK'); - // TODO: resend datagram, up to 3 times - this.transition('idle'); - this.emit('tunnelreqfailed', datagram); - }, 2000); - }, - _onExit() { - clearTimeout(this.tunnelingAckTimer); - }, - inbound_TUNNELING_ACK(datagram) { - this.log.debug( - util.format( - '===== datagram %d acknowledged by IP router', - datagram.tunnstate.seqnum - ) - ); - this.transition('idle'); - }, - ['*'](data) { - this.log.debug( - util.format( - '*** deferring %s until transition sendTunnReq_waitACK => idle', - data.inputType - ) - ); - this.deferUntilTransition('idle'); - }, - }, - - /* - * 2) INBOUND tunneling request (L_Data.ind) - only in tunnelling mode - */ - recvTunnReqIndication: { - _onEnter(datagram) { - this.seqnumRecv = datagram.tunnstate.seqnum; - this.acknowledge(datagram); - this.transition('idle'); - this.emitEvent(datagram); - }, - ['*'](data) { - this.log.debug(util.format('*** deferring Until Transition %j', data)); - this.deferUntilTransition('idle'); - }, - }, - }, - - acknowledge(datagram) { - const ack = this.prepareDatagram( - KnxConstants.SERVICE_TYPE.TUNNELING_ACK, - datagram - ); - /* acknowledge by copying the inbound datagram's sequence counter */ - ack.tunnstate.seqnum = datagram.tunnstate.seqnum; - this.send(ack, (err) => { - // TODO: handle send err - }); - }, - - emitEvent(datagram) { - // emit events to our beloved subscribers in a multitude of targets - // ORDER IS IMPORTANT! - const evtName = datagram.cemi.apdu.apci; - // 1. - // 'event_', ''GroupValue_Write', src, data - this.emit( - util.format('event_%s', datagram.cemi.dest_addr), - evtName, - datagram.cemi.src_addr, - datagram.cemi.apdu.data - ); - // 2. - // 'GroupValue_Write_1/2/3', src, data - this.emit( - util.format('%s_%s', evtName, datagram.cemi.dest_addr), - datagram.cemi.src_addr, - datagram.cemi.apdu.data - ); - // 3. - // 'GroupValue_Write', src, dest, data - this.emit( - evtName, - datagram.cemi.src_addr, - datagram.cemi.dest_addr, - datagram.cemi.apdu.data - ); - // 4. - // 'event', 'GroupValue_Write', src, dest, data - this.emit( - 'event', - evtName, - datagram.cemi.src_addr, - datagram.cemi.dest_addr, - datagram.cemi.apdu.data - ); - }, - - getLocalAddress() { - const candidateInterfaces = this.getIPv4Interfaces(); - // if user has declared a desired interface then use it - if (this.options && this.options.interface) { - const iface = candidateInterfaces[this.options.interface]; - if (!iface) - throw new Error( - 'Interface ' + - this.options.interface + - ' not found or has no useful IPv4 address!' - ); - - return candidateInterfaces[this.options.interface].address; - } - // just return the first available IPv4 non-loopback interface - const first = Object.values(candidateInterfaces)[0]; - if (first) return first.address; - - // no local IpV4 interfaces? - throw 'No valid IPv4 interfaces detected'; - }, - - // get the local address of the IPv4 interface we're going to use - getIPv4Interfaces() { - const candidateInterfaces = {}; - const interfaces = os.networkInterfaces(); - for (const [iface, addrs] of Object.entries(interfaces)) { - for (const addr of addrs) { - if ([4, 'IPv4'].indexOf(addr.family) > -1 && !addr.internal) { - this.log.trace( - util.format('candidate interface: %s (%j)', iface, addr) - ); - candidateInterfaces[iface] = addr; - } - } - } - return candidateInterfaces; - }, -}); diff --git a/src/FSM.ts b/src/FSM.ts new file mode 100644 index 0000000..0ae791a --- /dev/null +++ b/src/FSM.ts @@ -0,0 +1,666 @@ +import os from "os"; +import util from "util"; +import * as ipaddr from "ipaddr.js"; +import * as machina from "machina"; +import { keyText, KnxConstants } from "./KnxConstants.js"; +import IpRoutingConnection from "./IpRoutingConnection.js"; +import IpTunnelingConnection from "./IpTunnelingConnection.js"; +import KnxLog, { KnxLogOptions } from "./KnxLog.js"; +import EventEmitter from "events"; + +type KnxDeviceAddress = string; + +type KnxGroupAddress = string; + +/** The type of the KnxValue depends on the DPT that it is associated with */ +type KnxValue = number | string | boolean | Date; + +type HandlersSpec = { + connected?: () => void; + disconnected?: () => void; + event?: ( + evt: string, + src: KnxDeviceAddress, + dest: KnxGroupAddress, + value: Buffer + ) => void; + error?: (connstatus: any) => void; +}; + +export type KnxOptions = { + /** ip address of the KNX router or interface */ + ipAddr?: string; + /** port of the KNX router or interface */ + ipPort?: number; + /** in case you need to specify the multicast interface (say if you have more than one) */ + interface?: string; + /** the KNX physical address we'd like to use */ + physAddr?: string; + /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ + loglevel?: string; + /** do not automatically connect, but use connection.Connect() to establish connection */ + manualConnect?: boolean; + /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ + forceTunneling?: boolean; + /** wait at least 10 millisec between each datagram */ + minimumDelay?: number; + /** enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this */ + suppress_ack_ldatareq?: boolean; + /** 14/03/2020 In tunneling mode, echoes the sent message by emitting a new emitEvent, so other object with same group address, can receive the sent message. Default is false. */ + localEchoInTunneling?: boolean; + /** event handlers. You can also bind them later with connection.on(event, fn) */ + handlers?: HandlersSpec; +} & KnxLogOptions; + +export interface Datagram { + header_length: number; + protocol_version: number; + service_type: number; + total_length: number; + cemi?: { + dest_addr: string; + src_addr: string; + addinfo_length?: number; + apdu: { + apci: string; + data: any; + tpci: number; + bitlength?: number; + apdu_length?: number; + apdu_raw?: any; + }; + msgcode: number; + ctrl?: { + frameType: number; + reserved: number; + repeat: number; + broadcast: number; + priority: number; + acknowledge: number; + confirm: number; + destAddrType: number; + hopCount: number; + extendedFrame: number; + } + }; + tunnstate?: { + seqnum?: number; + channel_id: number; + tunnel_endpoint: string; + rsvd?: number; + }; + tunn?: { + protocol_type: number; + tunnel_endpoint: string; + } + hpai?: { + header_length?: number; + protocol_type: number; + tunnel_endpoint: string; + } + connstate?: { + state: number; + channel_id: number; + status?: number; + }, + cri?: { + connection_type: number; + knx_layer: number; + unused: number; + } +} + +export class KnxFSM extends machina.FSM { + private options: KnxOptions; + private log: any; + private ThreeLevelGroupAddressing: boolean; + private reconnection_cycles: number; + private sentTunnRequests: { [key: string]: Datagram }; + private useTunneling: boolean; + private remoteEndpoint: { + addrstring: string; + addr: any; + port: number; + }; + private localEchoInTunneling: boolean | undefined; + private channel_id?: any; + private conntime?: number; + private lastSentTime?: number; + private connecttimer?: NodeJS.Timeout; + private disconnecttimer?: NodeJS.Timeout; + private connstatetimer?: NodeJS.Timeout; + private idletimer?: NodeJS.Timeout; + private tunnelingAckTimer?: NodeJS.Timeout; + private seqnum: number; + private seqnumRecv: number; + + public localAddress: string | null; + + initialize(options: KnxOptions) { + this.options = options || {}; + this.log = KnxLog.get(options); + this.localAddress = null; + this.ThreeLevelGroupAddressing = true; + this.reconnection_cycles = 0; + this.sentTunnRequests = {}; + this.useTunneling = options.forceTunneling || false; + this.remoteEndpoint = { + addrstring: options.ipAddr || "224.0.23.12", + addr: ipaddr.parse(options.ipAddr || "224.0.23.12"), + port: options.ipPort || 3671, + }; + const range = this.remoteEndpoint.addr.range(); + this.localEchoInTunneling = + typeof options.localEchoInTunneling !== "undefined" + ? options.localEchoInTunneling + : false; + this.log.debug( + "initializing %s connection to %s", + range, + this.remoteEndpoint.addrstring + ); + switch (range) { + case "multicast": + if (this.localEchoInTunneling) { + this.localEchoInTunneling = false; + this.log.debug( + "localEchoInTunneling: true but DISABLED because i am on multicast" + ); + } + IpRoutingConnection(this); + break; + case "unicast": + case "private": + case "loopback": + this.useTunneling = true; + IpTunnelingConnection(this); + break; + default: + throw util.format( + "IP address % (%s) cannot be used for KNX", + options.ipAddr, + range + ); + } + } + + namespace: string = "knxnet"; + + initialState: string = "uninitialized"; + + states = { + uninitialized: { + ["*"]() { + this.transition("connecting"); + }, + }, + + jumptoconnecting: { + _onEnter() { + this.transition("connecting"); + }, + }, + + connecting: { + _onEnter() { + this.emit("disconnected"); + this.log.debug(util.format("useTunneling=%j", this.useTunneling)); + if (this.useTunneling) { + let connection_attempts = 0; + if (!this.localAddress) + throw "Not bound to an IPv4 non-loopback interface"; + this.log.debug( + util.format("Connecting via %s...", this.localAddress) + ); + this.connecttimer = setInterval(() => { + connection_attempts += 1; + if (connection_attempts >= 3) { + clearInterval(this.connecttimer); + if (this.remoteEndpoint.addr.range() == "multicast") { + this.log.warn( + "connection timed out, falling back to pure routing mode..." + ); + this.usingMulticastTunneling = true; + this.transition("connected"); + } else { + this.reconnection_cycles += 1; + const delay = Math.min(this.reconnection_cycles * 3, 300); + this.log.debug( + "reattempting connection in " + delay + " seconds" + ); + setTimeout( + () => this.transition("jumptoconnecting"), + delay * 1000 + ); + } + } else { + this.log.warn("connection timed out, retrying..."); + this.send( + this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) + ); + } + }, 3000); + delete this.channel_id; + delete this.conntime; + delete this.lastSentTime; + this.send( + this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) + ); + } else { + this.transition("connected"); + } + }, + _onExit() { + clearInterval(this.connecttimer); + }, + inbound_CONNECT_RESPONSE(datagram: any) { + this.log.debug(util.format("got connect response")); + if ( + datagram.hasOwnProperty("connstate") && + datagram.connstate.status === + KnxConstants.RESPONSECODE.E_NO_MORE_CONNECTIONS + ) { + try { + this.socket.close(); + } catch (error) {} + this.transition("uninitialized"); + this.emit("disconnected"); + this.log.debug( + "The KNXnet/IP server rejected the data connection (Maximum connections reached). Waiting 1 minute before retrying..." + ); + setTimeout(() => { + this.Connect(); + }, 60000); + } else { + this.channel_id = datagram.connstate.channel_id; + this.send( + this.prepareDatagram( + KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST + ) + ); + } + }, + inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { + if (this.useTunneling) { + const str = keyText("RESPONSECODE", datagram.connstate.status); + this.log.debug( + util.format( + "Got connection state response, connstate: %s, channel ID: %d", + str, + datagram.connstate.channel_id + ) + ); + this.transition("connected"); + } + }, + ["*"](data: any) { + this.log.debug(util.format("*** deferring Until Transition %j", data)); + this.deferUntilTransition("idle"); + }, + }, + + connected: { + _onEnter() { + this.reconnection_cycles = 0; + this.seqnum = -1; + this.lastSentTime = this.conntime = Date.now(); + this.log.debug( + util.format( + "--- Connected in %s mode ---", + this.useTunneling ? "TUNNELING" : "ROUTING" + ) + ); + this.transition("idle"); + this.emit("connected"); + }, + }, + + disconnecting: { + _onEnter() { + if (this.useTunneling) { + const aliveFor = this.conntime ? Date.now() - this.conntime : 0; + KnxLog.get().debug( + "(%s):\tconnection alive for %d seconds", + this.compositeState(), + aliveFor / 1000 + ); + this.disconnecttimer = setTimeout(() => { + KnxLog.get().debug( + "(%s):\tconnection timed out", + this.compositeState() + ); + try { + this.socket.close(); + } catch (error) {} + this.transition("uninitialized"); + this.emit("disconnected"); + }, 3000); + this.send( + this.prepareDatagram(KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST), + (err: any) => { + KnxLog.get().debug( + "(%s):\tsent DISCONNECT_REQUEST", + this.compositeState() + ); + } + ); + } + }, + _onExit() { + clearTimeout(this.disconnecttimer); + }, + inbound_DISCONNECT_RESPONSE(datagram: any) { + if (this.useTunneling) { + KnxLog.get().debug( + "(%s):\tgot disconnect response", + this.compositeState() + ); + try { + this.socket.close(); + } catch (error) {} + this.transition("uninitialized"); + this.emit("disconnected"); + } + }, + }, + + idle: { + _onEnter() { + if (this.useTunneling) { + if (this.idletimer == null) { + this.idletimer = setTimeout(() => { + this.transition("requestingConnState"); + clearTimeout(this.idletimer); + this.idletimer = null; + }, 60000); + } + } + KnxLog.get().debug("(%s):\t%s", this.compositeState(), " zzzz..."); + this.processQueue(); + }, + _onExit() {}, + outbound_ROUTING_INDICATION(datagram: Datagram) { + const elapsed = Date.now() - this.lastSentTime; + if ( + !this.options.minimumDelay || + elapsed >= this.options.minimumDelay + ) { + this.transition("sendDatagram", datagram); + } else { + setTimeout( + () => this.handle("outbound_ROUTING_INDICATION", datagram), + this.minimumDelay - elapsed + ); + } + }, + outbound_TUNNELING_REQUEST(datagram: Datagram) { + if (this.useTunneling) { + const elapsed = Date.now() - this.lastSentTime; + if ( + !this.options.minimumDelay || + elapsed >= this.options.minimumDelay + ) { + this.transition("sendDatagram", datagram); + } else { + setTimeout( + () => this.handle("outbound_TUNNELING_REQUEST", datagram), + this.minimumDelay - elapsed + ); + } + } else { + KnxLog.get().debug( + "(%s):\tdropping outbound TUNNELING_REQUEST, we're in routing mode", + this.compositeState() + ); + } + }, + ["inbound_TUNNELING_REQUEST_L_Data.ind"](datagram: Datagram) { + if (this.useTunneling) { + this.transition("recvTunnReqIndication", datagram); + } + }, + ["inbound_TUNNELING_REQUEST_L_Data.con"](datagram: Datagram) { + if (this.useTunneling) { + const confirmed = this.sentTunnRequests[datagram.cemi.dest_addr]; + if (confirmed) { + delete this.sentTunnRequests[datagram.cemi.dest_addr]; + this.emit("confirmed", confirmed); + } + KnxLog.get().trace( + "(%s): %s %s", + this.compositeState(), + datagram.cemi.dest_addr, + confirmed + ? "delivery confirmation (L_Data.con) received" + : "unknown dest addr" + ); + this.acknowledge(datagram); + } + }, + ["inbound_ROUTING_INDICATION_L_Data.ind"](datagram: Datagram) { + this.emitEvent(datagram); + }, + inbound_DISCONNECT_REQUEST(datagram: any) { + if (this.useTunneling) { + this.transition("connecting"); + } + }, + }, + + requestingConnState: { + _onEnter() { + KnxLog.get().debug("Requesting Connection State"); + KnxLog.get().trace( + "(%s): Requesting Connection State", + this.compositeState() + ); + this.send( + this.prepareDatagram( + KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST + ) + ); + this.connstatetimer = setTimeout(() => { + const msg = "timed out waiting for CONNECTIONSTATE_RESPONSE"; + KnxLog.get().trace("(%s): %s", this.compositeState(), msg); + this.transition("connecting"); + this.emit("error", msg); + }, 1000); + }, + _onExit() { + clearTimeout(this.connstatetimer); + }, + inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { + const state = keyText("RESPONSECODE", datagram.connstate.status); + switch (datagram.connstate.status) { + case 0: + this.transition("idle"); + break; + default: + this.log.debug( + util.format( + "*** error: %s *** (connstate.code: %d)", + state, + datagram.connstate.status + ) + ); + this.transition("connecting"); + this.emit("error", state); + } + }, + ["*"](data: any) { + this.log.debug( + util.format( + "*** deferring %s until transition from requestingConnState => idle", + data.inputType + ) + ); + this.deferUntilTransition("idle"); + }, + }, + + sendDatagram: { + _onEnter(datagram: Datagram) { + this.seqnum += 1; + if (this.useTunneling) datagram.tunnstate.seqnum = this.seqnum & 0xff; + this.send(datagram, (err: any) => { + if (err) { + this.seqnum -= 1; + this.transition("idle"); + } else { + if (this.useTunneling) + this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; + this.lastSentTime = Date.now(); + this.log.debug( + "(%s):\t>>>>>>> successfully sent seqnum: %d", + this.compositeState(), + this.seqnum + ); + if (this.useTunneling) { + this.transition("sendTunnReq_waitACK", datagram); + } else { + this.transition("idle"); + } + } + }); + }, + ["*"](data: any) { + this.log.debug( + util.format( + "*** deferring %s until transition sendDatagram => idle", + data.inputType + ) + ); + this.deferUntilTransition("idle"); + }, + }, + sendTunnReq_waitACK: { + _onEnter(datagram: Datagram) { + this.tunnelingAckTimer = setTimeout(() => { + this.log.debug("timed out waiting for TUNNELING_ACK"); + this.transition("idle"); + this.emit("tunnelreqfailed", datagram); + }, 2000); + }, + _onExit() { + clearTimeout(this.tunnelingAckTimer); + }, + inbound_TUNNELING_ACK(datagram: Datagram) { + this.log.debug( + util.format( + "===== datagram %d acknowledged by IP router", + datagram.tunnstate.seqnum + ) + ); + this.transition("idle"); + }, + ["*"](data: any) { + this.log.debug( + util.format( + "*** deferring %s until transition sendTunnReq_waitACK => idle", + data.inputType + ) + ); + this.deferUntilTransition("idle"); + }, + }, + recvTunnReqIndication: { + _onEnter(datagram: Datagram) { + this.seqnumRecv = datagram.tunnstate.seqnum; + this.acknowledge(datagram); + this.transition("idle"); + this.emitEvent(datagram); + }, + ["*"](data: any) { + this.log.debug(util.format("*** deferring Until Transition %j", data)); + this.deferUntilTransition("idle"); + }, + }, + }; + + acknowledge(datagram: Datagram) { + const ack = this.prepareDatagram( + KnxConstants.SERVICE_TYPE.TUNNELING_ACK, + datagram + ); + ack.tunnstate.seqnum = datagram.tunnstate.seqnum; + this.send(ack, (err: any) => {}); + } + + emitEvent(datagram: Datagram) { + const evtName = datagram.cemi.apdu.apci; + this.emit( + util.format("event_%s", datagram.cemi.dest_addr), + evtName, + datagram.cemi.src_addr, + datagram.cemi.apdu.data + ); + this.emit( + util.format("%s_%s", evtName, datagram.cemi.dest_addr), + datagram.cemi.src_addr, + datagram.cemi.apdu.data + ); + this.emit( + evtName, + datagram.cemi.src_addr, + datagram.cemi.dest_addr, + datagram.cemi.apdu.data + ); + this.emit( + "event", + evtName, + datagram.cemi.src_addr, + datagram.cemi.dest_addr, + datagram.cemi.apdu.data + ); + } + + getLocalAddress() { + const candidateInterfaces = this.getIPv4Interfaces(); + if (this.options && this.options.interface) { + const iface = candidateInterfaces[this.options.interface]; + if (!iface) + throw new Error( + "Interface " + + this.options.interface + + " not found or has no useful IPv4 address!" + ); + + return candidateInterfaces[this.options.interface].address; + } + const first = Object.values(candidateInterfaces)[0]; + if (first) return first.address; + + throw "No valid IPv4 interfaces detected"; + } + + getIPv4Interfaces() { + const candidateInterfaces: { [key: string]: any } = {}; + const interfaces = os.networkInterfaces(); + for (const [iface, addrs] of Object.entries(interfaces)) { + for (const addr of addrs) { + if ([4, "IPv4"].indexOf(addr.family) > -1 && !addr.internal) { + this.log.trace( + util.format("candidate interface: %s (%j)", iface, addr) + ); + candidateInterfaces[iface] = addr; + } + } + } + return candidateInterfaces; + } + + BindSocket(cb: (socket: any) => void) {} + + Connect() {} + + prepareDatagram( + serviceType: number, + datagram?: Datagram + ): Datagram { + return undefined as Datagram + } + + send(datagram: Datagram, cb?: (err: Error) => void) {} +} + +export default machina.FSM.extend(KnxFSM); diff --git a/src/IpRoutingConnection.js b/src/IpRoutingConnection.ts similarity index 66% rename from src/IpRoutingConnection.js rename to src/IpRoutingConnection.ts index 09117fe..afcaad7 100644 --- a/src/IpRoutingConnection.js +++ b/src/IpRoutingConnection.ts @@ -1,19 +1,12 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ +import * as util from 'util'; +import * as dgram from 'dgram'; +import KnxLog from './KnxLog.js'; +import KnxNet from './FSM.js'; -const util = require('util'); -const dgram = require('dgram'); -const KnxLog = require('./KnxLog.js'); -/** - Initializes a new KNX routing connection with provided values. Make - sure the local system allows UDP messages to the multicast group. -**/ -function IpRoutingConnection(instance) { +function IpRoutingConnection(instance: KnxNet): KnxNet { const log = KnxLog.get(); - instance.BindSocket = function (cb) { + instance.BindSocket = function (cb: (socket: dgram.Socket) => void): dgram.Socket { const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true }); udpSocket.on('listening', () => { log.debug( @@ -41,13 +34,13 @@ function IpRoutingConnection(instance) { // /// Start the connection /// - instance.Connect = function () { + instance.Connect = function (): KnxNet { this.localAddress = this.getLocalAddress(); - this.socket = this.BindSocket((socket) => { - socket.on('error', (errmsg) => + this.socket = this.BindSocket((socket: dgram.Socket) => { + socket.on('error', (errmsg: string) => log.debug(util.format('Socket error: %j', errmsg)) ); - socket.on('message', (msg, rinfo, callback) => { + socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo, callback: () => void) => { log.debug( 'Inbound multicast message from ' + rinfo.address + @@ -65,4 +58,4 @@ function IpRoutingConnection(instance) { return instance; } -module.exports = IpRoutingConnection; +export default IpRoutingConnection; \ No newline at end of file diff --git a/src/IpTunnelingConnection.js b/src/IpTunnelingConnection.ts similarity index 52% rename from src/IpTunnelingConnection.js rename to src/IpTunnelingConnection.ts index 32a3ca0..b58f807 100644 --- a/src/IpTunnelingConnection.js +++ b/src/IpTunnelingConnection.ts @@ -1,15 +1,12 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ +import dgram from 'dgram'; +import KnxLog from './KnxLog'; +import KnxNet from './FSM'; -const dgram = require('dgram'); -const KnxLog = require('./KnxLog.js'); -function IpTunnelingConnection(instance) { +function IpTunnelingConnection(instance: KnxNet): KnxNet { const log = KnxLog.get(); - instance.BindSocket = function (cb) { + instance.BindSocket = function (cb: (socket: dgram.Socket) => void): dgram.Socket { const udpSocket = dgram.createSocket('udp4'); udpSocket.bind(() => { log.debug( @@ -22,12 +19,12 @@ function IpTunnelingConnection(instance) { return udpSocket; }; - instance.Connect = function () { + instance.Connect = function (): IpTunnelingConnectionInstance { this.localAddress = this.getLocalAddress(); // create the socket - this.socket = this.BindSocket((socket) => { - socket.on('error', (errmsg) => log.debug('Socket error: %j', errmsg)); - socket.on('message', (msg, rinfo, callback) => { + this.socket = this.BindSocket((socket: dgram.Socket) => { + socket.on('error', (errmsg: string) => log.debug('Socket error: %j', errmsg)); + socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo, callback: () => void) => { log.debug('Inbound message: %s', msg.toString('hex')); this.onUdpSocketMessage(msg, rinfo, callback); }); @@ -40,4 +37,4 @@ function IpTunnelingConnection(instance) { return instance; } -module.exports = IpTunnelingConnection; +export default IpTunnelingConnection; \ No newline at end of file diff --git a/src/KnxConstants.js b/src/KnxConstants.ts similarity index 53% rename from src/KnxConstants.js rename to src/KnxConstants.ts index 63db91b..3d895b7 100644 --- a/src/KnxConstants.js +++ b/src/KnxConstants.ts @@ -3,12 +3,8 @@ * (C) 2016-2018 Elias Karakoulakis */ -const KnxLog = require('./KnxLog'); +import KnxLog from './KnxLog'; -// SOURCES: -// http://www.eb-systeme.de/?page_id=479 -// http://knxnetipdissect.sourceforge.net/doc.html -// http://dz.prosyst.com/pdoc/mBS_SH_SDK_7.3.0/modules/knx/api/com/prosyst/mbs/services/knx/ip/Constants.html const SERVICE_TYPE = { SEARCH_REQUEST: 0x0201, @@ -29,7 +25,7 @@ const SERVICE_TYPE = { ROUTING_LOST_MESSAGE: 0x0531, UNKNOWN: -1, }; -// + const CONNECTION_TYPE = { DEVICE_MGMT_CONNECTION: 0x03, TUNNEL_CONNECTION: 0x04, @@ -37,16 +33,16 @@ const CONNECTION_TYPE = { REMOTE_CONFIGURATION_CONNECTION: 0x07, OBJECT_SERVER_CONNECTION: 0x08, }; -// + const PROTOCOL_TYPE = { IPV4_UDP: 0x01, IPV4_TCP: 0x02, }; -// + const KNX_LAYER = { - LINK_LAYER: 0x02 /** Tunneling on link layer, establishes a link layer tunnel to the KNX network.*/, - RAW_LAYER: 0x04 /** Tunneling on raw layer, establishes a raw tunnel to the KNX network. */, - BUSMONITOR_LAYER: 0x80 /** Tunneling on busmonitor layer, establishes a busmonitor tunnel to the KNX network.*/, + LINK_LAYER: 0x02, + RAW_LAYER: 0x04, + BUSMONITOR_LAYER: 0x80, }; const FRAMETYPE = { @@ -54,19 +50,18 @@ const FRAMETYPE = { STANDARD: 0x01, }; -//https://github.com/calimero-project/calimero-core/blob/master/src/tuwien/auto/calimero/knxnetip/servicetype/ErrorCodes.java const RESPONSECODE = { - NO_ERROR: 0x00, // E_NO_ERROR - The connection was established succesfully + NO_ERROR: 0x00, E_HOST_PROTOCOL_TYPE: 0x01, E_VERSION_NOT_SUPPORTED: 0x02, E_SEQUENCE_NUMBER: 0x04, - E_CONNSTATE_LOST: 0x15, // typo in eibd/libserver/eibnetserver.cpp:394, forgot 0x prefix ??? "uchar res = 21;" - E_CONNECTION_ID: 0x21, // - The KNXnet/IP server device could not find an active data connection with the given ID - E_CONNECTION_TYPE: 0x22, // - The requested connection type is not supported by the KNXnet/IP server device - E_CONNECTION_OPTION: 0x23, // - The requested connection options is not supported by the KNXnet/IP server device - E_NO_MORE_CONNECTIONS: 0x24, // - The KNXnet/IP server could not accept the new data connection (Maximum reached) - E_DATA_CONNECTION: 0x26, // - The KNXnet/IP server device detected an erro concerning the Dat connection with the given ID - E_KNX_CONNECTION: 0x27, // - The KNXnet/IP server device detected an error concerning the KNX Bus with the given ID + E_CONNSTATE_LOST: 0x15, + E_CONNECTION_ID: 0x21, + E_CONNECTION_TYPE: 0x22, + E_CONNECTION_OPTION: 0x23, + E_NO_MORE_CONNECTIONS: 0x24, + E_DATA_CONNECTION: 0x26, + E_KNX_CONNECTION: 0x27, E_TUNNELING_LAYER: 0x29, }; @@ -80,10 +75,10 @@ const MESSAGECODES = { 'L_Raw.ind': 0x2d, 'L_Data.con': 0x2e, 'L_Raw.con': 0x2f, - 'ETS.Dummy1': 0xc1, // UNKNOWN: see https://bitbucket.org/ekarak/knx.js/issues/23 + 'ETS.Dummy1': 0xc1, }; -const APCICODES = [ +const APCICODES: string[] = [ 'GroupValue_Read', 'GroupValue_Response', 'GroupValue_Write', @@ -113,17 +108,17 @@ const KnxConstants = { }; /* TODO helper function to print enum keys */ -const keyText = (mapref, value) => { - // pass in map by name or value - const map = KnxConstants[mapref] || mapref; +const keyText = (mapref: string | object, value: number): string => { + const map = typeof mapref === 'string' ? KnxConstants[mapref] : mapref; + if (typeof map !== 'object') throw 'Unknown map: ' + mapref; for (const [key, v] of Object.entries(map)) if (v == value) return key; KnxLog.get().trace('not found: %j', value); }; -module.exports = { - ...KnxConstants, +export { + KnxConstants, APCICODES, keyText, -}; +}; \ No newline at end of file diff --git a/src/KnxLog.js b/src/KnxLog.js deleted file mode 100644 index 024e27f..0000000 --- a/src/KnxLog.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ -const util = require('util'); -let logger; - -const create = (options) => { - const level = - (options && (options.debug ? 'debug' : options.loglevel)) || 'info'; - //console.trace('new logger, level='+lvl); - return require('log-driver')({ - level, - format(lvl, msg /*string*/, ...a) { - // lvl is the log level ie 'debug' - const ts = new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''); - return a.length - ? // if more than one item to log, assume msg is a fmt string - util.format('[%s] %s ' + msg, lvl, ts, ...a) - : // otherwise, msg is a plain string - util.format('[%s] %s %s', lvl, ts, msg); - }, - }); -}; - -module.exports = { - get: (options) => logger || (logger = create(options)), -}; diff --git a/src/KnxLog.ts b/src/KnxLog.ts new file mode 100644 index 0000000..b97a8ac --- /dev/null +++ b/src/KnxLog.ts @@ -0,0 +1,54 @@ +import util from 'util'; +import factory from 'log-driver'; + +export enum LogLevel { + Trace = 'trace', + Debug = 'debug', + Info = 'info', + Warn = 'warn', + Error = 'error', +} + +export interface LogDriverOptions { + level: LogLevel; + format: (level: LogLevel, msg: string, ...args: any[]) => string; +} + +export interface KnxLogger { + get: (options?: KnxLogOptions) => Logger; +} + +export interface Logger { + trace: (...args: any[]) => void; + debug: (...args: any[]) => void; + info: (...args: any[]) => void; + warn: (...args: any[]) => void; + error: (...args: any[]) => void; + format: LogDriverOptions['format']; +} + +let logger: Logger; + + +export interface KnxLogOptions { + debug?: boolean; + loglevel?: LogLevel; +} + +const create = (options: KnxLogOptions): Logger => { + const level: LogLevel = + (options && (options.debug ? LogLevel.Debug : options.loglevel)) || LogLevel.Info; + return factory({ + level, + format(lvl: LogLevel, msg: string, ...a: any[]) { + const ts = new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''); + return a.length + ? util.format('[%s] %s ' + msg, lvl, ts, ...a) + : util.format('[%s] %s %s', lvl, ts, msg); + }, + }); +}; + +const KnxLog: KnxLogger = { get: (options: KnxLogOptions): Logger => logger || (logger = create(options)) }; + +export default KnxLog; \ No newline at end of file diff --git a/src/KnxProtocol.js b/src/KnxProtocol.ts similarity index 73% rename from src/KnxProtocol.js rename to src/KnxProtocol.ts index 54a3a7c..db33e52 100644 --- a/src/KnxProtocol.js +++ b/src/KnxProtocol.ts @@ -3,47 +3,56 @@ * (C) 2016-2018 Elias Karakoulakis */ -const util = require('util'); -const ipaddr = require('ipaddr.js'); -const Parser = require('binary-parser').Parser; -const BinaryProtocol = require('binary-protocol'); -const KnxProtocol = new BinaryProtocol(); -const KnxAddress = require('./Address'); -const KnxConstants = require('./KnxConstants'); -const KnxLog = require('./KnxLog'); +import util from "util"; +import ipaddr from "ipaddr.js"; +import { Parser } from 'binary-parser' +import { BinaryProtocol } from "binary-protocol"; +import KnxAddress from "./Address"; +import { APCICODES, keyText, KnxConstants } from "./KnxConstants"; +import KnxLog from "./KnxLog"; +import { Datagram } from "./FSM"; +export interface KnxProtocol extends BinaryProtocol { + lengths: { [key: string]: (value: any) => number }; + twoLevelAddressing: boolean; + debug: boolean; + apduStruct: Parser; +} + +const KnxProtocol: KnxProtocol = new BinaryProtocol() as KnxProtocol; // defaults KnxProtocol.twoLevelAddressing = false; KnxProtocol.lengths = {}; // TODO: Can this be a local variable, do we need to expose it? // helper function: what is the byte length of an object? -const knxlen = (objectName, context) => { +const knxlen = (objectName: string, context: any) => { const lf = KnxProtocol.lengths[objectName]; - return typeof lf === 'function' ? lf(context) : lf; + return typeof lf === "function" ? lf(context) : lf; }; -KnxProtocol.define('IPv4Endpoint', { - read(propertyName) { +KnxProtocol.define("IPv4Endpoint", { + read(propertyName: string) { this.pushStack({ addr: null, port: null }) - .raw('addr', 4) - .UInt16BE('port') + .raw("addr", 4) + .UInt16BE("port") .tap((hdr) => { hdr.addr = ipaddr.fromByteArray(hdr.addr); }) - .popStack(propertyName, (data) => data.addr.toString() + ':' + data.port); + .popStack(propertyName, (data) => data.addr.toString() + ":" + data.port); }, - write(value) { - if (!value) throw 'cannot write null value for IPv4Endpoint'; + write(value: string) { + if (!value) throw "cannot write null value for IPv4Endpoint"; - if (typeof value !== 'string' || !value.match(/\d*\.\d*\.\d*\.\d*:\d*/)) + if (typeof value !== "string" || !value.match(/\d*\.\d*\.\d*\.\d*:\d*/)) throw "Invalid IPv4 endpoint, please set a string as 'ip.add.re.ss:port'"; - const [addr, port] = value.split(':'); + const [addr, port] = value.split(":"); this.raw(Buffer.from(ipaddr.parse(addr).toByteArray()), 4); this.UInt16BE(port); }, }); -KnxProtocol.lengths['IPv4Endpoint'] = (value) => (value ? 6 : 0); + +KnxProtocol.lengths["IPv4Endpoint"] = (value: string) => (value ? 6 : 0); /* CRI: connection request/response */ // creq[22] = 0x04; /* structure len (4 bytes) */ @@ -51,80 +60,80 @@ KnxProtocol.lengths['IPv4Endpoint'] = (value) => (value ? 6 : 0); // creq[24] = 0x02; /* KNX Layer (Tunnel Link Layer) */ // creq[25] = 0x00; /* Reserved */ // ==> 4 bytes -KnxProtocol.define('CRI', { - read(propertyName) { +KnxProtocol.define("CRI", { + read(propertyName: string) { this.pushStack({ header_length: 0, connection_type: null, knx_layer: null, unused: null, }) // - .UInt8('header_length') - .UInt8('connection_type') - .UInt8('knx_layer') - .UInt8('unused') - .tap((hdr) => { + .UInt8("header_length") + .UInt8("connection_type") + .UInt8("knx_layer") + .UInt8("unused") + .tap((hdr: Datagram["cri"]) => { switch (hdr.connection_type) { case KnxConstants.CONNECTION_TYPE.DEVICE_MGMT_CONNECTION: break; // TODO case KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION: break; // TODO default: - throw 'Unsupported connection type: ' + hdr.connection_type; + throw "Unsupported connection type: " + hdr.connection_type; } }) .popStack(propertyName, (data) => { if (KnxProtocol.debug) - KnxLog.get().debug('read CRI: ' + JSON.stringify(data)); + KnxLog.get().debug("read CRI: " + JSON.stringify(data)); // pop the interim value off the stack and insert the real value into `propertyName` return data; }); }, - write(value) { + write(value: Datagram["cri"]) { if (!value) - return KnxLog.get().warn('CRI: cannot write null value for CRI'); + return KnxLog.get().warn("CRI: cannot write null value for CRI"); this.UInt8(0x04) // length .UInt8(value.connection_type) .UInt8(value.knx_layer) .UInt8(value.unused); }, }); -KnxProtocol.lengths['CRI'] = (value) => (value ? 4 : 0); +KnxProtocol.lengths["CRI"] = (value: Datagram["cri"]) => (value ? 4 : 0); // connection state response/request -KnxProtocol.define('ConnState', { - read(propertyName) { +KnxProtocol.define("ConnState", { + read(propertyName: string) { this.pushStack({ channel_id: null, status: null }) - .UInt8('channel_id') - .UInt8('status') - .popStack(propertyName, (data) => { - if (KnxProtocol.debug) KnxLog.get().trace('read ConnState: %j', data); + .UInt8("channel_id") + .UInt8("status") + .popStack(propertyName, (data: any) => { + if (KnxProtocol.debug) KnxLog.get().trace("read ConnState: %j", data); return data; }); }, - write(value) { + write(value: Datagram["connstate"]) { if (!value) - return KnxLog.get().error('cannot write null value for ConnState'); + return KnxLog.get().error("cannot write null value for ConnState"); this.UInt8(value.channel_id).UInt8(value.status); }, }); -KnxProtocol.lengths['ConnState'] = (value) => (value ? 2 : 0); +KnxProtocol.lengths["ConnState"] = (value: Datagram["connstate"]) => (value ? 2 : 0); // connection state response/request -KnxProtocol.define('TunnState', { - read(propertyName) { +KnxProtocol.define("TunnState", { + read(propertyName: string) { this.pushStack({ header_length: null, channel_id: null, seqnum: null, rsvd: null, }) - .UInt8('header_length') - .UInt8('channel_id') - .UInt8('seqnum') - .UInt8('rsvd') - .tap((hdr) => { - if (KnxProtocol.debug) KnxLog.get().trace('reading TunnState: %j', hdr); + .UInt8("header_length") + .UInt8("channel_id") + .UInt8("seqnum") + .UInt8("rsvd") + .tap((hdr: any) => { + if (KnxProtocol.debug) KnxLog.get().trace("reading TunnState: %j", hdr); switch (hdr.status) { case 0x00: break; @@ -133,19 +142,19 @@ KnxProtocol.define('TunnState', { }) .popStack(propertyName, (data) => data); }, - write(value) { + write(value: Datagram["tunnstate"]) { if (!value) return KnxLog.get().error( - 'TunnState: cannot write null value for TunnState' + "TunnState: cannot write null value for TunnState" ); - if (KnxProtocol.debug) KnxLog.get().trace('writing TunnState: %j', value); + if (KnxProtocol.debug) KnxLog.get().trace("writing TunnState: %j", value); this.UInt8(0x04) .UInt8(value.channel_id) .UInt8(value.seqnum) .UInt8(value.rsvd); }, }); -KnxProtocol.lengths['TunnState'] = (value) => (value ? 4 : 0); +KnxProtocol.lengths["TunnState"] = (value: Datagram["tunnstate"]) => (value ? 4 : 0); /* Connection HPAI */ // creq[6] = /* Host Protocol Address Information (HPAI) Lenght */ @@ -160,52 +169,52 @@ KnxProtocol.lengths['TunnState'] = (value) => (value ? 4 : 0); // creq[16-19] = /* IPv4 address */ // creq[20-21] = /* IPv4 local port number for TUNNELING requests */ // ==> 8 bytes -KnxProtocol.define('HPAI', { - read(propertyName) { +KnxProtocol.define("HPAI", { + read(propertyName: string) { this.pushStack({ header_length: 8, protocol_type: null, tunnel_endpoint: null, }) - .UInt8('header_length') - .UInt8('protocol_type') - .IPv4Endpoint('tunnel_endpoint') - .tap(function (hdr) { + .UInt8("header_length") + .UInt8("protocol_type") + .IPv4Endpoint("tunnel_endpoint") + .tap(function (hdr: Datagram["hpai"]) { if (this.buffer.length < hdr.header_length) { if (KnxProtocol.debug) KnxLog.get().trace( - '%d %d %d', + "%d %d %d", this.buffer.length, this.offset, hdr.header_length ); - throw 'Incomplete KNXNet HPAI header'; + throw "Incomplete KNXNet HPAI header"; } if (KnxProtocol.debug) { KnxLog.get().trace( - 'read HPAI: %j, proto = %s', + "read HPAI: %j, proto = %s", hdr, - KnxConstants.keyText('PROTOCOL_TYPE', hdr.protocol_type) + keyText("PROTOCOL_TYPE", hdr.protocol_type) ); } switch (hdr.protocol_type) { case KnxConstants.PROTOCOL_TYPE.IPV4_TCP: - throw 'TCP is not supported'; + throw "TCP is not supported"; default: } }) .popStack(propertyName, (data) => data); }, - write(value) { + write(value: Datagram["hpai"]) { if (!value) - return KnxLog.get().error('HPAI: cannot write null value for HPAI'); + return KnxLog.get().error("HPAI: cannot write null value for HPAI"); this.UInt8(0x08) // length: 8 bytes .UInt8(value.protocol_type) .IPv4Endpoint(value.tunnel_endpoint); }, }); -KnxProtocol.lengths['HPAI'] = (value) => { +KnxProtocol.lengths["HPAI"] = (value: Datagram["hpai"]) => { return value ? 8 : 0; }; @@ -345,23 +354,23 @@ Control Field 2 // control field const ctrlStruct = new Parser() // byte 1 - .bit1('frameType') - .bit1('reserved') - .bit1('repeat') - .bit1('broadcast') - .bit2('priority') - .bit1('acknowledge') - .bit1('confirm') + .bit1("frameType") + .bit1("reserved") + .bit1("repeat") + .bit1("broadcast") + .bit2("priority") + .bit1("acknowledge") + .bit1("confirm") // byte 2 - .bit1('destAddrType') - .bit3('hopCount') - .bit4('extendedFrame'); + .bit1("destAddrType") + .bit3("hopCount") + .bit4("extendedFrame"); // APDU: 2 bytes, tcpi = 6 bits, apci = 4 bits, remaining 6 bits = data (when length=1) -KnxProtocol.apduStruct = new Parser().bit6('tpci').bit4('apci').bit6('data'); +KnxProtocol.apduStruct = new Parser().bit6("tpci").bit4("apci").bit6("data"); -KnxProtocol.define('APDU', { - read(propertyName) { +KnxProtocol.define("APDU", { + read(propertyName: string) { this.pushStack({ apdu_length: null, apdu_raw: null, @@ -369,48 +378,48 @@ KnxProtocol.define('APDU', { apci: null, data: null, }) - .UInt8('apdu_length') - .tap(function (hdr) { + .UInt8("apdu_length") + .tap(function (hdr: Datagram["cemi"]["apdu"]) { //if (KnxProtocol.debug) KnxLog.get().trace('--- parsing extra %d apdu bytes', hdr.apdu_length+1); - this.raw('apdu_raw', hdr.apdu_length + 1); + this.raw("apdu_raw", hdr.apdu_length + 1); }) - .tap((hdr) => { + .tap((hdr: Datagram["cemi"]["apdu"]) => { // Parse the APDU. tcpi/apci bits split across byte boundary. // Typical example of protocol designed by committee. const apdu = KnxProtocol.apduStruct.parse(hdr.apdu_raw); hdr.tpci = apdu.tpci; - hdr.apci = KnxConstants.APCICODES[apdu.apci]; + hdr.apci = APCICODES[apdu.apci]; // APDU data should ALWAYS be a buffer, even for 1-bit payloads hdr.data = hdr.apdu_length > 1 ? hdr.apdu_raw.slice(2) : Buffer.from([apdu.data]); if (KnxProtocol.debug) - KnxLog.get().trace(' unmarshalled APDU: %j', hdr); + KnxLog.get().trace(" unmarshalled APDU: %j", hdr); }) .popStack(propertyName, (data) => data); }, - write(value) { - if (!value) throw 'cannot write null APDU value'; - const total_length = knxlen('APDU', value); + write(value: Datagram["cemi"]["apdu"]) { + if (!value) throw "cannot write null APDU value"; + const total_length = knxlen("APDU", value); //if (KnxProtocol.debug) KnxLog.get().trace('APDU.write: \t%j (total %d bytes)', value, total_length); - if (KnxConstants.APCICODES.indexOf(value.apci) == -1) - return KnxLog.get().error('invalid APCI code: %j', value); + if (APCICODES.indexOf(value.apci) == -1) + return KnxLog.get().error("invalid APCI code: %j", value); if (total_length < 3) - throw util.format('APDU is too small (%d bytes)', total_length); + throw util.format("APDU is too small (%d bytes)", total_length); if (total_length > 17) - throw util.format('APDU is too big (%d bytes)', total_length); + throw util.format("APDU is too big (%d bytes)", total_length); // camel designed by committee: total length MIGHT or MIGHT NOT include the payload // APDU length (1 byte) + TPCI/APCI: 6+4 bits + DATA: 6 bits (2 bytes) // OR: APDU length (1 byte) + TPCI/APCI: 6+4(+6 unused) bits (2bytes) + DATA: (1 to 14 bytes)) this.UInt8(total_length - 2); let word = - value.tpci * 0x400 + KnxConstants.APCICODES.indexOf(value.apci) * 0x40; + value.tpci * 0x400 + APCICODES.indexOf(value.apci) * 0x40; // if (total_length == 3) { // payload embedded in the last 6 bits word += parseInt( - isFinite(value.data) && typeof value.data !== 'object' + isFinite(value.data) && typeof value.data !== "object" ? value.data : value.data[0] ); @@ -426,7 +435,7 @@ KnxProtocol.define('APDU', { /* APDU length is truly chaotic: header and data can be interleaved (but not always!), so that apdu_length=1 means _2_ bytes following the apdu_length */ -KnxProtocol.lengths['APDU'] = (value) => { +KnxProtocol.lengths["APDU"] = (value) => { if (!value) return 0; // if we have the APDU bitlength, usually by the DPT, then simply use it if (value.bitlength || (value.data && value.data.bitlength)) { @@ -438,8 +447,8 @@ KnxProtocol.lengths['APDU'] = (value) => { // not all requests carry a value; eg read requests if (!value.data) value.data = 0; if (value.data.length) { - if (value.data.length < 1) throw 'APDU value is empty'; - if (value.data.length > 14) throw 'APDU value too big, must be <= 14 bytes'; + if (value.data.length < 1) throw "APDU value is empty"; + if (value.data.length > 14) throw "APDU value too big, must be <= 14 bytes"; if (value.data.length == 1) { const v = value.data[0]; if (!isNaN(parseFloat(v)) && isFinite(v) && v >= 0 && v <= 63) { @@ -458,17 +467,17 @@ KnxProtocol.lengths['APDU'] = (value) => { return 3; } else { KnxLog.get().warn( - 'Fix your code - APDU data payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes), got: %j (%s)', + "Fix your code - APDU data payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes), got: %j (%s)", value.data, typeof value.data ); - throw 'APDU payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes)'; + throw "APDU payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes)"; } } }; -KnxProtocol.define('CEMI', { - read(propertyName) { +KnxProtocol.define("CEMI", { + read(propertyName: string) { this.pushStack({ msgcode: 0, addinfo_length: -1, @@ -477,19 +486,19 @@ KnxProtocol.define('CEMI', { dest_addr: null, apdu: null, }) - .UInt8('msgcode') - .UInt8('addinfo_length') - .tap(function (hdr) { + .UInt8("msgcode") + .UInt8("addinfo_length") + .tap(function (hdr: Datagram["cemi"]) { if (hdr.addinfo_length !== 0) { - this.raw('addinfo', hdr.addinfo_length); + this.raw("addinfo", hdr.addinfo_length); } - }) - .raw('ctrl', 2) - .raw('src_addr', 2) - .raw('dest_addr', 2) - .tap(function (hdr) { + }) + .raw("ctrl", 2) + .raw("src_addr", 2) + .raw("dest_addr", 2) + .tap(function (hdr: Datagram["cemi"]) { // parse 16bit control field - hdr.ctrl = ctrlStruct.parse(hdr.ctrl); + hdr.ctrl = ctrlStruct.parse(hdr.ctrl as unknown as Buffer); // KNX source addresses are always physical hdr.src_addr = KnxAddress.toString( hdr.src_addr, @@ -500,21 +509,21 @@ KnxProtocol.define('CEMI', { hdr.ctrl.destAddrType ); switch (hdr.msgcode) { - case KnxConstants.MESSAGECODES['L_Data.req']: - case KnxConstants.MESSAGECODES['L_Data.ind']: - case KnxConstants.MESSAGECODES['L_Data.con']: { - this.APDU('apdu'); + case KnxConstants.MESSAGECODES["L_Data.req"]: + case KnxConstants.MESSAGECODES["L_Data.ind"]: + case KnxConstants.MESSAGECODES["L_Data.con"]: { + this.APDU("apdu"); if (KnxProtocol.debug) - KnxLog.get().trace('--- unmarshalled APDU ==> %j', hdr.apdu); + KnxLog.get().trace("--- unmarshalled APDU ==> %j", hdr.apdu); } } }) .popStack(propertyName, (data) => data); }, - write(value) { - if (!value) throw 'cannot write null CEMI value'; - if (KnxProtocol.debug) KnxLog.get().trace('CEMI.write: \n\t%j', value); - if (value.ctrl === null) throw 'no Control Field supplied'; + write(value: Datagram["cemi"]) { + if (!value) throw "cannot write null CEMI value"; + if (KnxProtocol.debug) KnxLog.get().trace("CEMI.write: \n\t%j", value); + if (value.ctrl === null) throw "no Control Field supplied"; const ctrlField1 = value.ctrl.frameType * 0x80 + value.ctrl.reserved * 0x40 + @@ -536,47 +545,47 @@ KnxProtocol.define('CEMI', { // only need to marshal an APDU if this is a // L_Data.* (requet/indication/confirmation) switch (value.msgcode) { - case KnxConstants.MESSAGECODES['L_Data.req']: - case KnxConstants.MESSAGECODES['L_Data.ind']: - case KnxConstants.MESSAGECODES['L_Data.con']: { - if (value.apdu === null) throw 'no APDU supplied'; + case KnxConstants.MESSAGECODES["L_Data.req"]: + case KnxConstants.MESSAGECODES["L_Data.ind"]: + case KnxConstants.MESSAGECODES["L_Data.con"]: { + if (value.apdu === null) throw "no APDU supplied"; this.APDU(value.apdu); } } }, }); -KnxProtocol.lengths['CEMI'] = (value) => { +KnxProtocol.lengths["CEMI"] = (value: Datagram["cemi"]) => { if (!value) return 0; - const apdu_length = knxlen('APDU', value.apdu); + const apdu_length = knxlen("APDU", value.apdu); if (KnxProtocol.debug) - KnxLog.get().trace('knxlen of cemi: %j == %d', value, 8 + apdu_length); + KnxLog.get().trace("knxlen of cemi: %j == %d", value, 8 + apdu_length); return 8 + apdu_length; }; -KnxProtocol.define('KNXNetHeader', { - read(propertyName) { +KnxProtocol.define("KNXNetHeader", { + read(propertyName: string) { this.pushStack({ header_length: 0, protocol_version: -1, service_type: -1, total_length: 0, }) - .UInt8('header_length') - .UInt8('protocol_version') - .UInt16BE('service_type') - .UInt16BE('total_length') - .tap(function (hdr) { - if (KnxProtocol.debug) KnxLog.get().trace('read KNXNetHeader :%j', hdr); + .UInt8("header_length") + .UInt8("protocol_version") + .UInt16BE("service_type") + .UInt16BE("total_length") + .tap(function (hdr: Datagram) { + if (KnxProtocol.debug) KnxLog.get().trace("read KNXNetHeader :%j", hdr); if (this.buffer.length + hdr.header_length < this.total_length) throw util.format( - 'Incomplete KNXNet packet: got %d bytes (expected %d)', + "Incomplete KNXNet packet: got %d bytes (expected %d)", this.buffer.length + hdr.header_length, this.total_length ); switch (hdr.service_type) { // case SERVICE_TYPE.SEARCH_REQUEST: case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: { - this.HPAI('hpai').HPAI('tunn').CRI('cri'); + this.HPAI("hpai").HPAI("tunn").CRI("cri"); break; } case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: @@ -584,30 +593,30 @@ KnxProtocol.define('KNXNetHeader', { case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: case KnxConstants.SERVICE_TYPE.DISCONNECT_RESPONSE: { - this.ConnState('connstate'); - if (hdr.total_length > 8) this.HPAI('hpai'); - if (hdr.total_length > 16) this.CRI('cri'); + this.ConnState("connstate"); + if (hdr.total_length > 8) this.HPAI("hpai"); + if (hdr.total_length > 16) this.CRI("cri"); break; } case KnxConstants.SERVICE_TYPE.DESCRIPTION_RESPONSE: { - this.raw('value', hdr.total_length); + this.raw("value", hdr.total_length); break; } // most common case: case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - this.TunnState('tunnstate'); - this.CEMI('cemi'); + this.TunnState("tunnstate"); + this.CEMI("cemi"); break; case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - this.TunnState('tunnstate'); + this.TunnState("tunnstate"); break; case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - this.CEMI('cemi'); + this.CEMI("cemi"); break; default: { KnxLog.get().warn( - 'read KNXNetHeader: unhandled serviceType = %s', - KnxConstants.keyText('SERVICE_TYPE', hdr.service_type) + "read KNXNetHeader: unhandled serviceType = %s", + keyText("SERVICE_TYPE", hdr.service_type) ); } } @@ -618,10 +627,10 @@ KnxProtocol.define('KNXNetHeader', { return data; }); }, - write(value) { - if (!value) throw 'cannot write null KNXNetHeader value'; - value.total_length = knxlen('KNXNetHeader', value); - if (KnxProtocol.debug) KnxLog.get().trace('writing KnxHeader:', value); + write(value: Datagram) { + if (!value) throw "cannot write null KNXNetHeader value"; + value.total_length = knxlen("KNXNetHeader", value); + if (KnxProtocol.debug) KnxLog.get().trace("writing KnxHeader:", value); this.UInt8(6) // header length (6 bytes constant) .UInt8(0x10) // protocol version 1.0 .UInt16BE(value.service_type) @@ -654,24 +663,24 @@ KnxProtocol.define('KNXNetHeader', { // case KnxConstants.SERVICE_TYPE.DESCRIPTION_RESPONSE: { default: { throw util.format( - 'write KNXNetHeader: unhandled serviceType = %s (%j)', - KnxConstants.keyText('SERVICE_TYPE', value), + "write KNXNetHeader: unhandled serviceType = %s (%j)", + keyText("SERVICE_TYPE", value.service_type), value ); } } }, }); -KnxProtocol.lengths['KNXNetHeader'] = (value) => { - if (!value) throw 'Must supply a valid KNXNetHeader value'; +KnxProtocol.lengths["KNXNetHeader"] = (value: Datagram) => { + if (!value) throw "Must supply a valid KNXNetHeader value"; switch (value.service_type) { //case SERVICE_TYPE.SEARCH_REQUEST: case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: return ( 6 + - knxlen('HPAI', value.hpai) + - knxlen('HPAI', value.tunn) + - knxlen('CRI', value.cri) + knxlen("HPAI", value.hpai) + + knxlen("HPAI", value.tunn) + + knxlen("CRI", value.cri) ); case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: @@ -679,18 +688,18 @@ KnxProtocol.lengths['KNXNetHeader'] = (value) => { case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: return ( 6 + - knxlen('ConnState', value.connstate) + - knxlen('HPAI', value.hpai) + - knxlen('CRI', value.cri) + knxlen("ConnState", value.connstate) + + knxlen("HPAI", value.hpai) + + knxlen("CRI", value.cri) ); case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: return ( - 6 + knxlen('TunnState', value.tunnstate) + knxlen('CEMI', value.cemi) + 6 + knxlen("TunnState", value.tunnstate) + knxlen("CEMI", value.cemi) ); case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - return 6 + knxlen('CEMI', value.cemi); + return 6 + knxlen("CEMI", value.cemi); } }; -module.exports = KnxProtocol; +export default KnxProtocol; diff --git a/src/devices/BinarySwitch.js b/src/devices/BinarySwitch.ts similarity index 68% rename from src/devices/BinarySwitch.js rename to src/devices/BinarySwitch.ts index 2c82079..1c25df0 100644 --- a/src/devices/BinarySwitch.js +++ b/src/devices/BinarySwitch.ts @@ -1,51 +1,56 @@ -/** - * knx.js - a pure Javascript library for KNX - * (C) 2016 Elias Karakoulakis - */ +import Datapoint from '../Datapoint'; +import KnxLog, { Logger } from '../KnxLog'; -const Datapoint = require('../Datapoint'); -const Log = require('../KnxLog'); +export default class BinarySwitch { + private control_ga: string; + private status_ga: string; + private conn: any; + private control: Datapoint; + private status: Datapoint; + private log: Logger; -class BinarySwitch { - constructor(options, conn) { + constructor(options: { ga: string, status_ga: string }, conn: any) { if (!options || !options.ga) throw 'must supply at least { ga }!'; this.control_ga = options.ga; this.status_ga = options.status_ga; if (conn) this.bind(conn); - this.log = Log.get(); + this.log = KnxLog.get(); } - bind(conn) { + + bind(conn: any) { if (!conn) this.log.warn('must supply a valid KNX connection to bind to'); this.conn = conn; this.control = new Datapoint({ ga: this.control_ga }, conn); if (this.status_ga) this.status = new Datapoint({ ga: this.status_ga }, conn); } + // EventEmitter proxy for status ga (if its set), otherwise proxy control ga - on(...args) { + on(...args: any[]) { const tgt = this.status_ga ? this.status : this.control; try { - tgt.on(...args); + tgt.on.apply(tgt, args); } catch (err) { this.log.error(err); } } + switchOn() { if (!this.conn) this.log.warn('must supply a valid KNX connection to bind to'); this.control.write(1); } + switchOff() { if (!this.conn) this.log.warn('must supply a valid KNX connection to bind to'); this.control.write(0); } - write(v) { + + write(v: any) { if (!this.conn) this.log.warn('must supply a valid KNX connection to bind to'); this.control.write(v); } -} - -module.exports = BinarySwitch; +} \ No newline at end of file diff --git a/src/devices/index.js b/src/devices/index.js deleted file mode 100644 index 94acf6a..0000000 --- a/src/devices/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.BinarySwitch = require('./BinarySwitch.js'); diff --git a/src/devices/index.ts b/src/devices/index.ts new file mode 100644 index 0000000..56cbf1c --- /dev/null +++ b/src/devices/index.ts @@ -0,0 +1,5 @@ +import BinarySwitch from './BinarySwitch'; + +export default { + BinarySwitch, +}; \ No newline at end of file diff --git a/src/dptlib/dpt1.js b/src/dptlib/dpt1.js deleted file mode 100644 index 71b888f..0000000 --- a/src/dptlib/dpt1.js +++ /dev/null @@ -1,223 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -const custom_truthiness = (value) => { - const f = parseFloat(value); - return !isNaN(f) && isFinite(f) - ? // numeric values (in native and string form) are truthy if NOT zero - f !== 0.0 - : // non-numeric value truthiness is Boolean true or the string 'true'. - value === true || value === 'true'; -}; - -exports.formatAPDU = (value) => Buffer.from([custom_truthiness(value)]); - -exports.fromBuffer = (buf) => { - if (buf.length !== 1) - return log.warn( - 'DPT1.fromBuffer: buf should be 1 byte (got %d bytes)', - buf.length - ); - return buf[0] !== 0; -}; - -// DPT basetype info hash -exports.basetype = { - bitlength: 1, - valuetype: 'basic', - desc: '1-bit value', -}; - -// DPT subtypes info hash -exports.subtypes = { - // 1.001 on/off - '001': { - use: 'G', - name: 'DPT_Switch', - desc: 'switch', - enc: { 0: 'Off', 1: 'On' }, - }, - - // 1.002 boolean - '002': { - use: 'G', - name: 'DPT_Bool', - desc: 'bool', - enc: { 0: 'false', 1: 'true' }, - }, - - // 1.003 enable - '003': { - use: 'G', - name: 'DPT_Enable', - desc: 'enable', - enc: { 0: 'disable', 1: 'enable' }, - }, - - // 1.004 ramp - '004': { - use: 'FB', - name: 'DPT_Ramp', - desc: 'ramp', - enc: { 0: 'No ramp', 1: 'Ramp' }, - }, - - // 1.005 alarm - '005': { - use: 'FB', - name: 'DPT_Alarm', - desc: 'alarm', - enc: { 0: 'No alarm', 1: 'Alarm' }, - }, - - // 1.006 binary value - '006': { - use: 'FB', - name: 'DPT_BinaryValue', - desc: 'binary value', - enc: { 0: 'Low', 1: 'High' }, - }, - - // 1.007 step - '007': { - use: 'FB', - name: 'DPT_Step', - desc: 'step', - enc: { 0: 'Decrease', 1: 'Increase' }, - }, - - // 1.008 up/down - '008': { - use: 'G', - name: 'DPT_UpDown', - desc: 'up/down', - enc: { 0: 'Up', 1: 'Down' }, - }, - - // 1.009 open/close - '009': { - use: 'G', - name: 'DPT_OpenClose', - desc: 'open/close', - enc: { 0: 'Open', 1: 'Close' }, - }, - - // 1.010 start/stop - '010': { - use: 'G', - name: 'DPT_Start', - desc: 'start/stop', - enc: { 0: 'Stop', 1: 'Start' }, - }, - - // 1.011 state - '011': { - use: 'FB', - name: 'DPT_State', - desc: 'state', - enc: { 0: 'Inactive', 1: 'Active' }, - }, - - // 1.012 invert - '012': { - use: 'FB', - name: 'DPT_Invert', - desc: 'invert', - enc: { 0: 'Not inverted', 1: 'inverted' }, - }, - - // 1.013 dim send style - '013': { - use: 'FB', - name: 'DPT_DimSendStyle', - desc: 'dim send style', - enc: { 0: 'Start/stop', 1: 'Cyclically' }, - }, - - // 1.014 input source - '014': { - use: 'FB', - name: 'DPT_InputSource', - desc: 'input source', - enc: { 0: 'Fixed', 1: 'Calculated' }, - }, - - // 1.015 reset - '015': { - use: 'G', - name: 'DPT_Reset', - desc: 'reset', - enc: { 0: 'no action(dummy)', 1: 'reset command(trigger)' }, - }, - - // 1.016 acknowledge - '016': { - use: 'G', - name: 'DPT_Ack', - desc: 'ack', - enc: { 0: 'no action(dummy)', 1: 'acknowledge command(trigger)' }, - }, - - // 1.017 trigger - '017': { - use: 'G', - name: 'DPT_Trigger', - desc: 'trigger', - enc: { 0: 'trigger', 1: 'trigger' }, - }, - - // 1.018 occupied - '018': { - use: 'G', - name: 'DPT_Occupancy', - desc: 'occupancy', - enc: { 0: 'not occupied', 1: 'occupied' }, - }, - - // 1.019 open window or door - '019': { - use: 'G', - name: 'DPT_WindowDoor', - desc: 'open window/door', - enc: { 0: 'closed', 1: 'open' }, - }, - - // 1.021 and/or - '021': { - use: 'FB', - name: 'DPT_LogicalFunction', - desc: 'and/or', - enc: { 0: 'logical function OR', 1: 'logical function AND' }, - }, - - // 1.022 scene A/B - '022': { - use: 'FB', - name: 'DPT_Scene_AB', - desc: 'scene A/B', - enc: { 0: 'scene A', 1: 'scene B' }, - }, - - // 1.023 shutter/blinds mode - '023': { - use: 'FB', - name: 'DPT_ShutterBlinds_Mode', - desc: 'shutter/blinds mode', - enc: { - 0: 'only move Up/Down mode (shutter)', - 1: 'move Up/Down + StepStop mode (blind)', - }, - }, - - // 1.100 cooling/heating ---FIXME--- - 100: { - use: '???', - name: 'DPT_Heat/Cool', - desc: 'heat/cool', - enc: { 0: '???', 1: '???' }, - }, -}; diff --git a/src/dptlib/dpt1.ts b/src/dptlib/dpt1.ts new file mode 100644 index 0000000..42e2e7c --- /dev/null +++ b/src/dptlib/dpt1.ts @@ -0,0 +1,235 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +const custom_truthiness = (value: string | boolean): boolean => { + const f = parseFloat(value as string); + return !isNaN(f) && isFinite(f) + ? // numeric values (in native and string form) are truthy if NOT zero + f !== 0.0 + : // non-numeric value truthiness is Boolean true or the string 'true'. + value === true || value === "true"; +}; + +const config: DatapointConfig = { + id: "dpt1", + formatAPDU: (value) => Buffer.from([custom_truthiness(value) ? 1 : 0]), + + fromBuffer: (buf) => { + if (buf.length !== 1) { + log.warn( + "DPT1.fromBuffer: buf should be 1 byte (got %d bytes)", + buf.length + ); + + return null; + } + + return buf[0] !== 0; + }, + + // DPT basetype info hash + basetype: { + bitlength: 1, + valuetype: "basic", + desc: "1-bit value", + }, + + // DPT subtypes info hash + subtypes: { + // 1.001 on/off + "001": { + use: "G", + name: "DPT_Switch", + desc: "switch", + enc: { 0: "Off", 1: "On" }, + }, + + // 1.002 boolean + "002": { + use: "G", + name: "DPT_Bool", + desc: "bool", + enc: { 0: "false", 1: "true" }, + }, + + // 1.003 enable + "003": { + use: "G", + name: "DPT_Enable", + desc: "enable", + enc: { 0: "disable", 1: "enable" }, + }, + + // 1.004 ramp + "004": { + use: "FB", + name: "DPT_Ramp", + desc: "ramp", + enc: { 0: "No ramp", 1: "Ramp" }, + }, + + // 1.005 alarm + "005": { + use: "FB", + name: "DPT_Alarm", + desc: "alarm", + enc: { 0: "No alarm", 1: "Alarm" }, + }, + + // 1.006 binary value + "006": { + use: "FB", + name: "DPT_BinaryValue", + desc: "binary value", + enc: { 0: "Low", 1: "High" }, + }, + + // 1.007 step + "007": { + use: "FB", + name: "DPT_Step", + desc: "step", + enc: { 0: "Decrease", 1: "Increase" }, + }, + + // 1.008 up/down + "008": { + use: "G", + name: "DPT_UpDown", + desc: "up/down", + enc: { 0: "Up", 1: "Down" }, + }, + + // 1.009 open/close + "009": { + use: "G", + name: "DPT_OpenClose", + desc: "open/close", + enc: { 0: "Open", 1: "Close" }, + }, + + // 1.010 start/stop + "010": { + use: "G", + name: "DPT_Start", + desc: "start/stop", + enc: { 0: "Stop", 1: "Start" }, + }, + + // 1.011 state + "011": { + use: "FB", + name: "DPT_State", + desc: "state", + enc: { 0: "Inactive", 1: "Active" }, + }, + + // 1.012 invert + "012": { + use: "FB", + name: "DPT_Invert", + desc: "invert", + enc: { 0: "Not inverted", 1: "inverted" }, + }, + + // 1.013 dim send style + "013": { + use: "FB", + name: "DPT_DimSendStyle", + desc: "dim send style", + enc: { 0: "Start/stop", 1: "Cyclically" }, + }, + + // 1.014 input source + "014": { + use: "FB", + name: "DPT_InputSource", + desc: "input source", + enc: { 0: "Fixed", 1: "Calculated" }, + }, + + // 1.015 reset + "015": { + use: "G", + name: "DPT_Reset", + desc: "reset", + enc: { 0: "no action(dummy)", 1: "reset command(trigger)" }, + }, + + // 1.016 acknowledge + "016": { + use: "G", + name: "DPT_Ack", + desc: "ack", + enc: { 0: "no action(dummy)", 1: "acknowledge command(trigger)" }, + }, + + // 1.017 trigger + "017": { + use: "G", + name: "DPT_Trigger", + desc: "trigger", + enc: { 0: "trigger", 1: "trigger" }, + }, + + // 1.018 occupied + "018": { + use: "G", + name: "DPT_Occupancy", + desc: "occupancy", + enc: { 0: "not occupied", 1: "occupied" }, + }, + + // 1.019 open window or door + "019": { + use: "G", + name: "DPT_WindowDoor", + desc: "open window/door", + enc: { 0: "closed", 1: "open" }, + }, + + // 1.021 and/or + "021": { + use: "FB", + name: "DPT_LogicalFunction", + desc: "and/or", + enc: { 0: "logical function OR", 1: "logical function AND" }, + }, + + // 1.022 scene A/B + "022": { + use: "FB", + name: "DPT_Scene_AB", + desc: "scene A/B", + enc: { 0: "scene A", 1: "scene B" }, + }, + + // 1.023 shutter/blinds mode + "023": { + use: "FB", + name: "DPT_ShutterBlinds_Mode", + desc: "shutter/blinds mode", + enc: { + 0: "only move Up/Down mode (shutter)", + 1: "move Up/Down + StepStop mode (blind)", + }, + }, + + // 1.100 cooling/heating ---FIXME--- + 100: { + use: "???", + name: "DPT_Heat/Cool", + desc: "heat/cool", + enc: { 0: "???", 1: "???" }, + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt10.js b/src/dptlib/dpt10.js deleted file mode 100644 index 92a83d4..0000000 --- a/src/dptlib/dpt10.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT10.*: time (3 bytes) -// -const util = require('util'); -const dowTimeRegexp = /((\d)\/)?(\d{1,2}):(\d{1,2}):(\d{1,2})/; - -// DPTFrame to parse a DPT10 frame. -// Always 8-bit aligned. - -exports.formatAPDU = (value) => { - let dow, hour, minute, second; - // day of week. NOTE: JS Sunday = 0 - switch (typeof value) { - case 'string': - // try to parse - match = dowTimeRegexp.exec(value); - if (match) { - const currentDoW = ((new Date().getDay() - 7) % 7) + 7; - dow = match[2] != undefined ? parseInt(match[2]) : currentDoW; - hour = parseInt(match[3]); - minute = parseInt(match[4]); - second = parseInt(match[5]); - } else { - log.warn('DPT10: invalid time format (%s)', value); - } - break; - case 'object': - if (value.constructor.name != 'Date') { - log.warn('Must supply a Date or String for DPT10 time'); - break; - } - case 'number': - value = new Date(value); - default: - dow = ((value.getDay() - 7) % 7) + 7; - hour = value.getHours(); - minute = value.getMinutes(); - second = value.getSeconds(); - } - - return Buffer.from([(dow << 5) + hour, minute, second]); -}; - -// return a JS Date from a DPT10 payload, with DOW/hour/month/seconds set to the buffer values. -// The week/month/year are inherited from the current timestamp. -exports.fromBuffer = (buf) => { - if (buf.length != 3) return log.warn('DPT10: Buffer should be 3 bytes long'); - const [dnh, minutes, seconds] = buf; - const dow = (dnh & 0b11100000) >> 5; - const hours = dnh & 0b00011111; - if ( - hours < 0 || - hours > 23 || - minutes < 0 || - minutes > 59 || - seconds < 0 || - seconds > 59 - ) - return log.warn( - 'DPT10: buffer %j (decoded as %d:%d:%d) is not a valid time', - buf, - hours, - minutes, - seconds - ); - - const d = new Date(); - if (d.getDay() !== dow) - // adjust day of month to get the day of week right - d.setDate(d.getDate() + dow - d.getDay()); - // TODO: Shouldn't this be UTCHours? - d.setHours(hours, minutes, seconds); - return d; -}; - -// DPT10 base type info -exports.basetype = { - bitlength: 24, - valuetype: 'composite', - desc: 'day of week + time of day', -}; - -// DPT10 subtypes info -exports.subtypes = { - // 10.001 time of day - '001': { - name: 'DPT_TimeOfDay', - desc: 'time of day', - }, -}; diff --git a/src/dptlib/dpt10.ts b/src/dptlib/dpt10.ts new file mode 100644 index 0000000..d0fb973 --- /dev/null +++ b/src/dptlib/dpt10.ts @@ -0,0 +1,105 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT10.*: time (3 bytes) +// +const dowTimeRegexp = /((\d)\/)?(\d{1,2}):(\d{1,2}):(\d{1,2})/; + +// DPTFrame to parse a DPT10 frame. +// Always 8-bit aligned. + +const config: DatapointConfig = { + id: "dpt10", + formatAPDU: (value) => { + let dow: number, hour: number, minute: number, second: number; + // day of week. NOTE: JS Sunday = 0 + switch (typeof value) { + case "string": + // try to parse + const match = dowTimeRegexp.exec(value); + if (match) { + const currentDoW = ((new Date().getDay() - 7) % 7) + 7; + dow = match[2] != undefined ? parseInt(match[2]) : currentDoW; + hour = parseInt(match[3]); + minute = parseInt(match[4]); + second = parseInt(match[5]); + } else { + log.warn("DPT10: invalid time format (%s)", value); + } + break; + case "object": + if (value.constructor.name != "Date") { + log.warn("Must supply a Date or String for DPT10 time"); + break; + } + case "number": + value = new Date(value); + default: + dow = ((value.getDay() - 7) % 7) + 7; + hour = value.getHours(); + minute = value.getMinutes(); + second = value.getSeconds(); + } + + return Buffer.from([(dow << 5) + hour, minute, second]); + }, + + // return a JS Date from a DPT10 payload, with DOW/hour/month/seconds set to the buffer values. + // The week/month/year are inherited from the current timestamp. + fromBuffer: (buf) => { + if (buf.length != 3) + return log.warn("DPT10: Buffer should be 3 bytes long"); + const [dnh, minutes, seconds] = buf; + const dow = (dnh & 0b11100000) >> 5; + const hours = dnh & 0b00011111; + if ( + hours < 0 || + hours > 23 || + minutes < 0 || + minutes > 59 || + seconds < 0 || + seconds > 59 + ) + return log.warn( + "DPT10: buffer %j (decoded as %d:%d:%d) is not a valid time", + buf, + hours, + minutes, + seconds + ); + + const d = new Date(); + if (d.getDay() !== dow) + // adjust day of month to get the day of week right + d.setDate(d.getDate() + dow - d.getDay()); + // TODO: Shouldn't this be UTCHours? + d.setHours(hours, minutes, seconds); + return d; + }, + + // DPT10 base type info + basetype: { + bitlength: 24, + valuetype: "composite", + desc: "day of week + time of day", + }, + + // DPT10 subtypes info + subtypes: { + // 10.001 time of day + "001": { + name: "DPT_TimeOfDay", + desc: "time of day", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt11.js b/src/dptlib/dpt11.js deleted file mode 100644 index 3b47bdf..0000000 --- a/src/dptlib/dpt11.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; -const util = require('util'); -// -// DPT11.*: date -// -exports.formatAPDU = (value) => { - if (value == null) return log.error('cannot write null value for DPT11'); - switch (typeof value) { - case 'string': - case 'number': - value = new Date(value); - break; - case 'object': - // this expects the month property to be zero-based (January = 0, etc.) - if (value instanceof Date) break; - const { year, month, day } = value; - value = new Date(parseInt(year), parseInt(month), parseInt(day)); - } - if (isNaN(value.getDate())) - return log.error( - 'Must supply a numeric timestamp, Date or String object for DPT11 Date' - ); - - const year = value.getFullYear(); - return Buffer.from([ - value.getDate(), - value.getMonth() + 1, - year - (year >= 2000 ? 2000 : 1900), - ]); -}; - -exports.fromBuffer = (buf) => { - if (buf.length != 3) return log.error('Buffer should be 3 bytes long'); - const day = buf[0] & 31; //0b00011111); - const month = buf[1] & 15; //0b00001111); - let year = buf[2] & 127; //0b01111111); - year = year + (year > 89 ? 1900 : 2000); - if ( - day < 1 || - day > 31 || - month < 1 || - month > 12 || - year < 1990 || - year > 2089 - ) { - log.error( - '%j => %d/%d/%d is not valid date according to DPT11, setting to 1990/01/01', - buf, - day, - month, - year - ); - //return new Date(1990, 01, 01); - throw new Error('Error converting date buffer to Date object.'); - } - return new Date(year, month - 1, day); -}; - -// DPT11 base type info -exports.basetype = { - bitlength: 24, - valuetype: 'composite', - desc: '3-byte date value', -}; - -// DPT11 subtypes info -exports.subtypes = { - // 11.001 date - '001': { - name: 'DPT_Date', - desc: 'Date', - }, -}; diff --git a/src/dptlib/dpt11.ts b/src/dptlib/dpt11.ts new file mode 100644 index 0000000..90271d9 --- /dev/null +++ b/src/dptlib/dpt11.ts @@ -0,0 +1,85 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); +// +// DPT11.*: date +// +const config: DatapointConfig = { + id: "dpt11", + formatAPDU: (value) => { + if (value == null) return log.error("cannot write null value for DPT11"); + switch (typeof value) { + case "string": + case "number": + value = new Date(value); + break; + case "object": + // this expects the month property to be zero-based (January = 0, etc.) + if (value instanceof Date) break; + const { year, month, day } = value; + value = new Date(parseInt(year), parseInt(month), parseInt(day)); + } + if (isNaN(value.getDate())) + return log.error( + "Must supply a numeric timestamp, Date or String object for DPT11 Date" + ); + + const year = value.getFullYear(); + return Buffer.from([ + value.getDate(), + value.getMonth() + 1, + year - (year >= 2000 ? 2000 : 1900), + ]); + }, + + fromBuffer: (buf) => { + if (buf.length != 3) return log.error("Buffer should be 3 bytes long"); + const day = buf[0] & 31; //0b00011111); + const month = buf[1] & 15; //0b00001111); + let year = buf[2] & 127; //0b01111111); + year = year + (year > 89 ? 1900 : 2000); + if ( + day < 1 || + day > 31 || + month < 1 || + month > 12 || + year < 1990 || + year > 2089 + ) { + log.error( + "%j => %d/%d/%d is not valid date according to DPT11, setting to 1990/01/01", + buf, + day, + month, + year + ); + //return new Date(1990, 01, 01); + throw new Error("Error converting date buffer to Date object."); + } + return new Date(year, month - 1, day); + }, + + // DPT11 base type info + basetype: { + bitlength: 24, + valuetype: "composite", + desc: "3-byte date value", + }, + + // DPT11 subtypes info + subtypes: { + // 11.001 date + "001": { + name: "DPT_Date", + desc: "Date", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt12.js b/src/dptlib/dpt12.js deleted file mode 100644 index 7534e2a..0000000 --- a/src/dptlib/dpt12.js +++ /dev/null @@ -1,25 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT12.*: 4-byte unsigned value -// - - -// DPT12 base type info -exports.basetype = { - bitlength : 32, - signedness: "unsigned", - valuetype : "basic", - desc : "4-byte unsigned value" -} - -// DPT12 subtype info -exports.subtypes = { - // 12.001 counter pulses - "001" : { - "name" : "DPT_Value_4_Ucount", "desc" : "counter pulses" - } -} diff --git a/src/dptlib/dpt12.ts b/src/dptlib/dpt12.ts new file mode 100644 index 0000000..9a2ba77 --- /dev/null +++ b/src/dptlib/dpt12.ts @@ -0,0 +1,31 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// +// DPT12.*: 4-byte unsigned value +// + +// DPT12 base type info +const config: DatapointConfig = { + id: "dpt12", + basetype: { + bitlength: 32, + signedness: "unsigned", + valuetype: "basic", + desc: "4-byte unsigned value", + }, + // DPT12 subtype info + subtypes: { + // 12.001 counter pulses + "001": { + name: "DPT_Value_4_Ucount", + desc: "counter pulses", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt13.js b/src/dptlib/dpt13.js deleted file mode 100644 index e8e412c..0000000 --- a/src/dptlib/dpt13.js +++ /dev/null @@ -1,73 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT13: 4-byte signed value -// - -// DPT13 base type info -exports.basetype = { - "bitlength" : 32, - "signedness": "signed", - "valuetype" : "basic", - "desc" : "4-byte signed value", - "range" : [-Math.pow(2, 31), Math.pow(2, 31)-1] -} - -// DPT13 subtypes -exports.subtypes = { - // 13.001 counter pulses (signed) - "001" : { - "name" : "DPT_Value_4_Count", "desc" : "counter pulses (signed)", - "unit" : "pulses" - }, - - "002" : { - "name" : "DPT_Value_Activation_Energy", "desc" : "activation energy (J/mol)" , - "unit" : "J/mol" - }, - - // 13.010 active energy (Wh) - "010" : { - "name" : "DPT_ActiveEnergy", "desc" : "active energy (Wh)", - "unit" : "Wh" - }, - - // 13.011 apparent energy (VAh) - "011" : { - "name" : "DPT_ApparantEnergy", "desc" : "apparent energy (VAh)", - "unit" : "VAh" - }, - - // 13.012 reactive energy (VARh) - "012" : { - "name" : "DPT_ReactiveEnergy", "desc" : "reactive energy (VARh)", - "unit" : "VARh" - }, - - // 13.013 active energy (KWh) - "013" : { - "name" : "DPT_ActiveEnergy_kWh", "desc" : "active energy (kWh)", - "unit" : "kWh" - }, - - // 13.014 apparent energy (kVAh) - "014" : { - "name" : "DPT_ApparantEnergy_kVAh", "desc" : "apparent energy (kVAh)", - "unit" : "VAh" - }, - - // 13.015 reactive energy (kVARh) - "015" : { - "name" : "DPT_ReactiveEnergy_kVARh", "desc" : "reactive energy (kVARh)", - "unit" : "kVARh" - }, - - // 13.100 time lag(s) - "100" : { - "name" : "DPT_LongDeltaTimeSec", "desc" : "time lag(s)", - "unit" : "s" - }, -} diff --git a/src/dptlib/dpt13.ts b/src/dptlib/dpt13.ts new file mode 100644 index 0000000..76e7e68 --- /dev/null +++ b/src/dptlib/dpt13.ts @@ -0,0 +1,89 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// +// DPT13: 4-byte signed value +// + +// DPT13 base type info +const config: DatapointConfig = { + id: "dpt13", + basetype: { + bitlength: 32, + signedness: "signed", + valuetype: "basic", + desc: "4-byte signed value", + range: [-Math.pow(2, 31), Math.pow(2, 31) - 1], + }, + + // DPT13 subtypes + subtypes: { + // 13.001 counter pulses (signed) + "001": { + name: "DPT_Value_4_Count", + desc: "counter pulses (signed)", + unit: "pulses", + }, + + "002": { + name: "DPT_Value_Activation_Energy", + desc: "activation energy (J/mol)", + unit: "J/mol", + }, + + // 13.010 active energy (Wh) + "010": { + name: "DPT_ActiveEnergy", + desc: "active energy (Wh)", + unit: "Wh", + }, + + // 13.011 apparent energy (VAh) + "011": { + name: "DPT_ApparantEnergy", + desc: "apparent energy (VAh)", + unit: "VAh", + }, + + // 13.012 reactive energy (VARh) + "012": { + name: "DPT_ReactiveEnergy", + desc: "reactive energy (VARh)", + unit: "VARh", + }, + + // 13.013 active energy (KWh) + "013": { + name: "DPT_ActiveEnergy_kWh", + desc: "active energy (kWh)", + unit: "kWh", + }, + + // 13.014 apparent energy (kVAh) + "014": { + name: "DPT_ApparantEnergy_kVAh", + desc: "apparent energy (kVAh)", + unit: "VAh", + }, + + // 13.015 reactive energy (kVARh) + "015": { + name: "DPT_ReactiveEnergy_kVARh", + desc: "reactive energy (kVARh)", + unit: "kVARh", + }, + + // 13.100 time lag(s) + "100": { + name: "DPT_LongDeltaTimeSec", + desc: "time lag(s)", + unit: "s", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt14.js b/src/dptlib/dpt14.js deleted file mode 100644 index 2dbdc18..0000000 --- a/src/dptlib/dpt14.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT14.*: 4-byte floating point value -// - -/* In sharp contrast to DPT9 (16-bit floating point - JS spec does not support), - * the case for 32-bit floating point is simple... - */ - -exports.formatAPDU = (value) => { - if (value == null || typeof value != 'number') - log.error('DPT14: Must supply a number value'); - const apdu_data = Buffer.alloc(4); - apdu_data.writeFloatBE(value, 0); - return apdu_data; -}; - -exports.fromBuffer = (buf) => { - if (buf.length != 4) log.warn('DPT14: Buffer should be 4 bytes long'); - return buf.readFloatBE(0); -}; - -// DPT14 base type info -exports.basetype = { - bitlength: 32, - valuetype: 'basic', - range: [0, Math.pow(2, 32)], - desc: '32-bit floating point value', -}; - -// DPT14 subtypes info -exports.subtypes = { - // TODO - '007': { - name: 'DPT_Value_AngleDeg°', - desc: 'angle, degree', - unit: '°', - }, - - '019': { - name: 'DPT_Value_Electric_Current', - desc: 'electric current', - unit: 'A', - }, - - '027': { - name: 'DPT_Value_Electric_Potential', - desc: 'electric potential', - unit: 'V', - }, - - '028': { - name: 'DPT_Value_Electric_PotentialDifference', - desc: 'electric potential difference', - unit: 'V', - }, - - '031': { - name: 'DPT_Value_Energ', - desc: 'energy', - unit: 'J', - }, - - '032': { - name: 'DPT_Value_Force', - desc: 'force', - unit: 'N', - }, - - '033': { - name: 'DPT_Value_Frequency', - desc: 'frequency', - unit: 'Hz', - }, - - '036': { - name: 'DPT_Value_Heat_FlowRate', - desc: 'heat flow rate', - unit: 'W', - }, - - '037': { - name: 'DPT_Value_Heat_Quantity', - desc: 'heat, quantity of', - unit: 'J', - }, - - '038': { - name: 'DPT_Value_Impedance', - desc: 'impedance', - unit: 'Ω', - }, - - '039': { - name: 'DPT_Value_Length', - desc: 'length', - unit: 'm', - }, - - '051': { - name: 'DPT_Value_Mass', - desc: 'mass', - unit: 'kg', - }, - - '056': { - name: 'DPT_Value_Power', - desc: 'power', - unit: 'W', - }, - - '065': { - name: 'DPT_Value_Speed', - desc: 'speed', - unit: 'm/s', - }, - - '066': { - name: 'DPT_Value_Stress', - desc: 'stress', - unit: 'Pa', - }, - - '067': { - name: 'DPT_Value_Surface_Tension', - desc: 'surface tension', - unit: '1/Nm', - }, - - '068': { - name: 'DPT_Value_Common_Temperature', - desc: 'temperature, common', - unit: '°C', - }, - - '069': { - name: 'DPT_Value_Absolute_Temperature', - desc: 'temperature (absolute)', - unit: 'K', - }, - - '070': { - name: 'DPT_Value_TemperatureDifference', - desc: 'temperature difference', - unit: 'K', - }, - - '078': { - name: 'DPT_Value_Weight', - desc: 'weight', - unit: 'N', - }, - - '079': { - name: 'DPT_Value_Work', - desc: 'work', - unit: 'J', - }, -}; diff --git a/src/dptlib/dpt14.ts b/src/dptlib/dpt14.ts new file mode 100644 index 0000000..5c24f9d --- /dev/null +++ b/src/dptlib/dpt14.ts @@ -0,0 +1,171 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT14.*: 4-byte floating point value +// + +/* In sharp contrast to DPT9 (16-bit floating point - JS spec does not support), + * the case for 32-bit floating point is simple... + */ +const config: DatapointConfig = { + id: "dpt14", + formatAPDU: (value) => { + if (value == null || typeof value != "number") + log.error("DPT14: Must supply a number value"); + const apdu_data = Buffer.alloc(4); + apdu_data.writeFloatBE(value, 0); + return apdu_data; + }, + + fromBuffer: (buf) => { + if (buf.length != 4) log.warn("DPT14: Buffer should be 4 bytes long"); + return buf.readFloatBE(0); + }, + + // DPT14 base type info + basetype: { + bitlength: 32, + valuetype: "basic", + range: [0, Math.pow(2, 32)], + desc: "32-bit floating point value", + }, + + // DPT14 subtypes info + subtypes: { + // TODO + "007": { + name: "DPT_Value_AngleDeg°", + desc: "angle, degree", + unit: "°", + }, + + "019": { + name: "DPT_Value_Electric_Current", + desc: "electric current", + unit: "A", + }, + + "027": { + name: "DPT_Value_Electric_Potential", + desc: "electric potential", + unit: "V", + }, + + "028": { + name: "DPT_Value_Electric_PotentialDifference", + desc: "electric potential difference", + unit: "V", + }, + + "031": { + name: "DPT_Value_Energ", + desc: "energy", + unit: "J", + }, + + "032": { + name: "DPT_Value_Force", + desc: "force", + unit: "N", + }, + + "033": { + name: "DPT_Value_Frequency", + desc: "frequency", + unit: "Hz", + }, + + "036": { + name: "DPT_Value_Heat_FlowRate", + desc: "heat flow rate", + unit: "W", + }, + + "037": { + name: "DPT_Value_Heat_Quantity", + desc: "heat, quantity of", + unit: "J", + }, + + "038": { + name: "DPT_Value_Impedance", + desc: "impedance", + unit: "Ω", + }, + + "039": { + name: "DPT_Value_Length", + desc: "length", + unit: "m", + }, + + "051": { + name: "DPT_Value_Mass", + desc: "mass", + unit: "kg", + }, + + "056": { + name: "DPT_Value_Power", + desc: "power", + unit: "W", + }, + + "065": { + name: "DPT_Value_Speed", + desc: "speed", + unit: "m/s", + }, + + "066": { + name: "DPT_Value_Stress", + desc: "stress", + unit: "Pa", + }, + + "067": { + name: "DPT_Value_Surface_Tension", + desc: "surface tension", + unit: "1/Nm", + }, + + "068": { + name: "DPT_Value_Common_Temperature", + desc: "temperature, common", + unit: "°C", + }, + + "069": { + name: "DPT_Value_Absolute_Temperature", + desc: "temperature (absolute)", + unit: "K", + }, + + "070": { + name: "DPT_Value_TemperatureDifference", + desc: "temperature difference", + unit: "K", + }, + + "078": { + name: "DPT_Value_Weight", + desc: "weight", + unit: "N", + }, + + "079": { + name: "DPT_Value_Work", + desc: "work", + unit: "J", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt15.js b/src/dptlib/dpt15.js deleted file mode 100644 index b50e64c..0000000 --- a/src/dptlib/dpt15.js +++ /dev/null @@ -1,24 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT15.*: Access data -// - -// TODO: implement fromBuffer, formatAPDU - -// DPT15 base type info -exports.basetype = { - "bitlength" : 32, - "valuetype" : "basic", - "desc" : "4-byte access control data" -} - -// DPT15 subtypes info -exports.subtypes = { - "000" : { - "name" : "DPT_Access_Data" - } -} diff --git a/src/dptlib/dpt15.ts b/src/dptlib/dpt15.ts new file mode 100644 index 0000000..dbbed94 --- /dev/null +++ b/src/dptlib/dpt15.ts @@ -0,0 +1,33 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// +// DPT15.*: Access data +// + +// TODO: implement fromBuffer, formatAPDU + +// DPT15 base type info + +const config: DatapointConfig = { + id: "dpt15", + basetype: { + bitlength: 32, + valuetype: "basic", + desc: "4-byte access control data", + }, + + // DPT15 subtypes info + subtypes: { + "000": { + name: "DPT_Access_Data", + desc: "Access Data", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt16.js b/src/dptlib/dpt16.js deleted file mode 100644 index 142d099..0000000 --- a/src/dptlib/dpt16.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT16: ASCII string (max 14 chars) -// - -exports.formatAPDU = (value) => { - if (typeof value !== 'string') return log.warn('Must supply a string value'); - - const buf = Buffer.alloc(14); - buf.write(value, 'ascii'); - return buf; -}; - -exports.fromBuffer = (buf) => buf.toString('ascii'); -// DPT16 basetype info -exports.basetype = { - bitlength: 14 * 8, - valuetype: 'basic', - desc: '14-character string', -}; - -// DPT9 subtypes -exports.subtypes = { - // 16.000 ASCII string - '000': { - use: 'G', - name: 'DPT_String_ASCII', - desc: 'ASCII string', - force_encoding: 'US-ASCII', - }, - - // 16.001 ISO-8859-1 string - '001': { - use: 'G', - name: 'DPT_String_8859_1', - desc: 'ISO-8859-1 string', - force_encoding: 'ISO-8859-1', - }, -}; diff --git a/src/dptlib/dpt16.ts b/src/dptlib/dpt16.ts new file mode 100644 index 0000000..6218d61 --- /dev/null +++ b/src/dptlib/dpt16.ts @@ -0,0 +1,54 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT16: ASCII string (max 14 chars) +// + +const config: DatapointConfig = { + id: "dpt16", + formatAPDU: (value) => { + if (typeof value !== "string") + return log.warn("Must supply a string value"); + + const buf = Buffer.alloc(14); + buf.write(value, "ascii"); + return buf; + }, + + fromBuffer: (buf) => buf.toString("ascii"), + // DPT16 basetype info + basetype: { + bitlength: 14 * 8, + valuetype: "basic", + desc: "14-character string", + }, + + // DPT9 subtypes + subtypes: { + // 16.000 ASCII string + "000": { + use: "G", + name: "DPT_String_ASCII", + desc: "ASCII string", + force_encoding: "US-ASCII", + }, + + // 16.001 ISO-8859-1 string + "001": { + use: "G", + name: "DPT_String_8859_1", + desc: "ISO-8859-1 string", + force_encoding: "ISO-8859-1", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt17.js b/src/dptlib/dpt17.js deleted file mode 100644 index 29aa625..0000000 --- a/src/dptlib/dpt17.js +++ /dev/null @@ -1,25 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT17: Scene number -// - -// TODO: implement fromBuffer, formatAPDU - -// DPT17 basetype info -exports.basetype = { - bitlength : 8, - valuetype : 'basic', - desc : "scene number" -} - -// DPT17 subtypes -exports.subtypes = { - // 17.001 Scene number - "001" : { use : "G", - name : "DPT_SceneNumber", desc : "Scene Number", - }, -} diff --git a/src/dptlib/dpt17.ts b/src/dptlib/dpt17.ts new file mode 100644 index 0000000..693bd07 --- /dev/null +++ b/src/dptlib/dpt17.ts @@ -0,0 +1,29 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// +// DPT17: Scene number +// + +// TODO: implement fromBuffer, formatAPDU + +const config: DatapointConfig = { + id: "dpt17", + // DPT17 basetype info + basetype: { + bitlength: 8, + valuetype: "basic", + desc: "scene number", + }, + // DPT17 subtypes + subtypes: { + // 17.001 Scene number + "001": { use: "G", name: "DPT_SceneNumber", desc: "Scene Number" }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt18.js b/src/dptlib/dpt18.js deleted file mode 100644 index e0ef060..0000000 --- a/src/dptlib/dpt18.js +++ /dev/null @@ -1,84 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT18: 8-bit Scene Control -// - -/* - class DPT18_Frame < DPTFrame - bit1 :exec_learn, { - :display_name : "Execute=0, Learn = 1" - } - bit1 :pad, { - :display_name : "Reserved bit" - } - bit6 :data, { - :display_name : "Scene number" - } - end -*/ - -// TODO: implement fromBuffer, formatAPDU -const log = require('log-driver').logger; - -exports.formatAPDU = function (value) { - if (value == null) log.warn("DPT18: cannot write null value"); - else { - var apdu_data = new Buffer(1); - if (typeof value == 'object' && - value.hasOwnProperty("save_recall") && - value.hasOwnProperty("scenenumber")) { - var sSceneNumberbinary = ((value.scenenumber - 1) >>> 0).toString(2); - var sVal = value.save_recall + "0" + sSceneNumberbinary.padStart(6, "0"); - //console.log("BANANA SEND HEX " + sVal.toString("hex").toUpperCase()) - apdu_data[0] = parseInt(sVal, 2);// 0b10111111; - } else { - log.error("DPT18: Must supply a value object of {save_recall, scenenumber}"); - } - return apdu_data; - } -} - -exports.fromBuffer = function (buf) { - //console.log("BANANA BUFF RECEIVE HEX " + buf.toString("hex").toUpperCase()) - if (buf.length != 1) { - log.error("DP18: Buffer should be 1 byte long"); - } else { - var sBit = (parseInt(buf.toString("hex").toUpperCase(), 16).toString(2)).padStart(8, '0'); // Get bit from hex - //console.log("BANANA BUFF RECEIVE BIT " + sBit) - return { - save_recall: sBit.substring(0, 1), - scenenumber: parseInt(sBit.substring(2), 2) + 1 - } - }; -} - -// DPT18 basetype info -exports.basetype = { - bitlength: 8, - valuetype: 'composite', - desc: "8-bit Scene Activate/Learn + number" -} - -// DPT9 subtypes -exports.subtypes = { - // 9.001 temperature (oC) - "001": { - name: "DPT_SceneControl", desc: "scene control" - } -} - - - -/* -02/April/2020 Supergiovane -USE: -Input must be an object: {save_recall, scenenumber} -save_recall: 0 = recall scene, 1 = save scene -scenenumber: the scene number, example 1 -Example: {save_recall=0, scenenumber=2} -*/ - diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts new file mode 100644 index 0000000..336021e --- /dev/null +++ b/src/dptlib/dpt18.ts @@ -0,0 +1,96 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; +// +// DPT18: 8-bit Scene Control +// + +/* + class DPT18_Frame < DPTFrame + bit1 :exec_learn, { + :display_name : "Execute=0, Learn = 1" + } + bit1 :pad, { + :display_name : "Reserved bit" + } + bit6 :data, { + :display_name : "Scene number" + } + end +*/ + +// TODO: implement fromBuffer, formatAPDU + +const log = logger.get(); + +const config: DatapointConfig = { + id: "dpt18", + formatAPDU: function (value) { + if (value == null) log.warn("DPT18: cannot write null value"); + else { + var apdu_data = new Buffer(1); + if ( + typeof value == "object" && + value.hasOwnProperty("save_recall") && + value.hasOwnProperty("scenenumber") + ) { + var sSceneNumberbinary = ((value.scenenumber - 1) >>> 0).toString(2); + var sVal = + value.save_recall + "0" + sSceneNumberbinary.padStart(6, "0"); + //console.log("BANANA SEND HEX " + sVal.toString("hex").toUpperCase()) + apdu_data[0] = parseInt(sVal, 2); // 0b10111111; + } else { + log.error( + "DPT18: Must supply a value object of {save_recall, scenenumber}" + ); + } + return apdu_data; + } + }, + + fromBuffer: function (buf) { + //console.log("BANANA BUFF RECEIVE HEX " + buf.toString("hex").toUpperCase()) + if (buf.length != 1) { + log.error("DP18: Buffer should be 1 byte long"); + } else { + var sBit = parseInt(buf.toString("hex").toUpperCase(), 16) + .toString(2) + .padStart(8, "0"); // Get bit from hex + //console.log("BANANA BUFF RECEIVE BIT " + sBit) + return { + save_recall: sBit.substring(0, 1), + scenenumber: parseInt(sBit.substring(2), 2) + 1, + }; + } + }, + // DPT18 basetype info + basetype: { + bitlength: 8, + valuetype: "composite", + desc: "8-bit Scene Activate/Learn + number", + }, + + // DPT9 subtypes + subtypes: { + // 9.001 temperature (oC) + "001": { + name: "DPT_SceneControl", + desc: "scene control", + }, + }, +}; + +export default config; + +/* +02/April/2020 Supergiovane +USE: +Input must be an object: {save_recall, scenenumber} +save_recall: 0 = recall scene, 1 = save scene +scenenumber: the scene number, example 1 +Example: {save_recall=0, scenenumber=2} +*/ diff --git a/src/dptlib/dpt19.js b/src/dptlib/dpt19.js deleted file mode 100644 index 863ccda..0000000 --- a/src/dptlib/dpt19.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// TODO: implement fromBuffer, formatAPDU - -// -// DPT19: 8-byte Date and Time -// - -exports.formatAPDU = (value) => { - if (!(value instanceof Date)) - return log.error('DPT19: Must supply a Date object'); - - // Sunday is 0 in Javascript, but 7 in KNX. - const day = value.getDay() === 0 ? 7 : value.getDay(); - return Buffer.from([ - value.getFullYear() - 1900, - value.getMonth() + 1, - value.getDate(), - (day << 5) + value.getHours(), - value.getMinutes(), - value.getSeconds(), - 0, - 0, - ]); -}; - -exports.fromBuffer = (buf) => { - if (buf.length !== 8) return log.warn('DPT19: Buffer should be 8 bytes long'); - return new Date( - buf[0] + 1900, - buf[1] - 1, - buf[2], - buf[3] & 0b00011111, - buf[4], - buf[5] - ); -}; - -exports.basetype = { - bitlength: 64, - valuetype: 'composite', - desc: '8-byte Date+Time', -}; - -exports.subtypes = { - // 19.001 - '001': { - name: 'DPT_DateTime', - desc: 'datetime', - }, -}; diff --git a/src/dptlib/dpt19.ts b/src/dptlib/dpt19.ts new file mode 100644 index 0000000..bc85e53 --- /dev/null +++ b/src/dptlib/dpt19.ts @@ -0,0 +1,64 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); +// TODO: implement fromBuffer, formatAPDU + +// +// DPT19: 8-byte Date and Time +// + +const config: DatapointConfig = { + id: "dpt19", + formatAPDU: (value) => { + if (!(value instanceof Date)) + return log.error("DPT19: Must supply a Date object"); + + // Sunday is 0 in Javascript, but 7 in KNX. + const day = value.getDay() === 0 ? 7 : value.getDay(); + return Buffer.from([ + value.getFullYear() - 1900, + value.getMonth() + 1, + value.getDate(), + (day << 5) + value.getHours(), + value.getMinutes(), + value.getSeconds(), + 0, + 0, + ]); + }, + + fromBuffer: (buf) => { + if (buf.length !== 8) + return log.warn("DPT19: Buffer should be 8 bytes long"); + return new Date( + buf[0] + 1900, + buf[1] - 1, + buf[2], + buf[3] & 0b00011111, + buf[4], + buf[5] + ); + }, + + basetype: { + bitlength: 64, + valuetype: "composite", + desc: "8-byte Date+Time", + }, + + subtypes: { + // 19.001 + "001": { + name: "DPT_DateTime", + desc: "datetime", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt2.js b/src/dptlib/dpt2.js deleted file mode 100644 index 25e5812..0000000 --- a/src/dptlib/dpt2.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// DPT2 frame description. -// Always 8-bit aligned. -exports.formatAPDU = (value) => { - if (value == null) return log.error('DPT2: cannot write null value'); - - if ( - typeof value === 'object' && - value.hasOwnProperty('priority') && - value.hasOwnProperty('data') - ) - return Buffer.from([(value.priority << 1) + (value.data & 0b00000001)]); - - log.error('DPT2: Must supply an value {priority:, data:}'); - // FIXME: should this return zero buffer when error? Or nothing? - return Buffer.from([0]); -}; - -exports.fromBuffer = (buf) => { - if (buf.length !== 1) return log.error('Buffer should be 1 byte long'); - - return { - priority: (buf[0] & 0b00000010) >> 1, - data: buf[0] & 0b00000001, - }; -}; - -// DPT basetype info hash -exports.basetype = { - bitlength: 2, - valuetype: 'composite', - desc: '1-bit value with priority', -}; - -// DPT subtypes info hash -exports.subtypes = { - // 2.001 switch control - '001': { - use: 'G', - name: 'DPT_Switch_Control', - desc: 'switch with priority', - enc: { 0: 'Off', 1: 'On' }, - }, - // 2.002 boolean control - '002': { - use: 'G', - name: 'DPT_Bool_Control', - desc: 'boolean with priority', - enc: { 0: 'false', 1: 'true' }, - }, - // 2.003 enable control - '003': { - use: 'FB', - name: 'DPT_Emable_Control', - desc: 'enable with priority', - enc: { 0: 'Disabled', 1: 'Enabled' }, - }, - - // 2.004 ramp control - '004': { - use: 'FB', - name: 'DPT_Ramp_Control', - desc: 'ramp with priority', - enc: { 0: 'No ramp', 1: 'Ramp' }, - }, - - // 2.005 alarm control - '005': { - use: 'FB', - name: 'DPT_Alarm_Control', - desc: 'alarm with priority', - enc: { 0: 'No alarm', 1: 'Alarm' }, - }, - - // 2.006 binary value control - '006': { - use: 'FB', - name: 'DPT_BinaryValue_Control', - desc: 'binary value with priority', - enc: { 0: 'Off', 1: 'On' }, - }, - - // 2.007 step control - '007': { - use: 'FB', - name: 'DPT_Step_Control', - desc: 'step with priority', - enc: { 0: 'Off', 1: 'On' }, - }, - - // 2.008 Direction1 control - '008': { - use: 'FB', - name: 'DPT_Direction1_Control', - desc: 'direction 1 with priority', - enc: { 0: 'Off', 1: 'On' }, - }, - - // 2.009 Direction2 control - '009': { - use: 'FB', - name: 'DPT_Direction2_Control', - desc: 'direction 2 with priority', - enc: { 0: 'Off', 1: 'On' }, - }, - - // 2.010 start control - '001': { - use: 'FB', - name: 'DPT_Start_Control', - desc: 'start with priority', - enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, - }, - - // 2.011 state control - '001': { - use: 'FB', - name: 'DPT_Switch_Control', - desc: 'switch', - enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, - }, - - // 2.012 invert control - '001': { - use: 'FB', - name: 'DPT_Switch_Control', - desc: 'switch', - enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, - }, -}; diff --git a/src/dptlib/dpt2.ts b/src/dptlib/dpt2.ts new file mode 100644 index 0000000..4cd490e --- /dev/null +++ b/src/dptlib/dpt2.ts @@ -0,0 +1,152 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; +import KnxLog from "../KnxLog"; + +const log = KnxLog.get(); + +interface Dpt2Value { + priority: boolean | number; + data: boolean | number; +} + +const config: DatapointConfig = { + id: "dpt2", + + // DPT2 frame description. + // Always 8-bit aligned. + formatAPDU: (value: Dpt2Value) => { + if (value == null) return log.error("DPT2: cannot write null value"); + + if ( + typeof value === "object" && + value.hasOwnProperty("priority") && + value.hasOwnProperty("data") + ) + return Buffer.from([ + ((value.priority as number) << 1) + (value.data as number & 0b00000001), + ]); + + log.error("DPT2: Must supply an value {priority:, data:}"); + // FIXME: should this return zero buffer when error? Or nothing? + return Buffer.from([0]); + }, + + fromBuffer: (buf) => { + if (buf.length !== 1) return log.error("Buffer should be 1 byte long"); + + return { + priority: (buf[0] & 0b00000010) >> 1, + data: buf[0] & 0b00000001, + }; + }, + + // DPT basetype info hash + basetype: { + bitlength: 2, + valuetype: "composite", + desc: "1-bit value with priority", + }, + + // DPT subtypes info hash + subtypes: { + // 2.001 switch control + "001": { + use: "G", + name: "DPT_Switch_Control", + desc: "switch with priority", + enc: { 0: "Off", 1: "On" }, + }, + // 2.002 boolean control + "002": { + use: "G", + name: "DPT_Bool_Control", + desc: "boolean with priority", + enc: { 0: "false", 1: "true" }, + }, + // 2.003 enable control + "003": { + use: "FB", + name: "DPT_Emable_Control", + desc: "enable with priority", + enc: { 0: "Disabled", 1: "Enabled" }, + }, + + // 2.004 ramp control + "004": { + use: "FB", + name: "DPT_Ramp_Control", + desc: "ramp with priority", + enc: { 0: "No ramp", 1: "Ramp" }, + }, + + // 2.005 alarm control + "005": { + use: "FB", + name: "DPT_Alarm_Control", + desc: "alarm with priority", + enc: { 0: "No alarm", 1: "Alarm" }, + }, + + // 2.006 binary value control + "006": { + use: "FB", + name: "DPT_BinaryValue_Control", + desc: "binary value with priority", + enc: { 0: "Off", 1: "On" }, + }, + + // 2.007 step control + "007": { + use: "FB", + name: "DPT_Step_Control", + desc: "step with priority", + enc: { 0: "Off", 1: "On" }, + }, + + // 2.008 Direction1 control + "008": { + use: "FB", + name: "DPT_Direction1_Control", + desc: "direction 1 with priority", + enc: { 0: "Off", 1: "On" }, + }, + + // 2.009 Direction2 control + "009": { + use: "FB", + name: "DPT_Direction2_Control", + desc: "direction 2 with priority", + enc: { 0: "Off", 1: "On" }, + }, + + // 2.010 start control + "010": { + use: "FB", + name: "DPT_Start_Control", + desc: "start with priority", + enc: { 0: "No control", 1: "No control", 2: "Off", 3: "On" }, + }, + + // 2.011 state control + "011": { + use: "FB", + name: "DPT_Switch_Control", + desc: "switch", + enc: { 0: "No control", 1: "No control", 2: "Off", 3: "On" }, + }, + + // 2.012 invert control + "012": { + use: "FB", + name: "DPT_Switch_Control", + desc: "switch", + enc: { 0: "No control", 1: "No control", 2: "Off", 3: "On" }, + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt20.js b/src/dptlib/dpt20.js deleted file mode 100644 index cb239ca..0000000 --- a/src/dptlib/dpt20.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT20: 1-byte HVAC -// -// FIXME: help needed -exports.formatAPDU = (value) => { - log.debug('./knx/src/dpt20.js : input value = ' + value); - return Buffer.from([value]); -}; - -exports.fromBuffer = (buf) => { - if (buf.length !== 1) throw 'Buffer should be 1 bytes long'; - const ret = buf.readUInt8(0); - log.debug(' dpt20.js fromBuffer : ' + ret); - return ret; -}; - -exports.basetype = { - bitlength: 8, - range: [,], - valuetype: 'basic', - desc: '1-byte', -}; - -exports.subtypes = { - // 20.102 HVAC mode - 102: { - name: 'HVAC_Mode', - desc: '', - unit: '', - scalar_range: [,], - range: [,], - }, -}; diff --git a/src/dptlib/dpt20.ts b/src/dptlib/dpt20.ts new file mode 100644 index 0000000..faefc24 --- /dev/null +++ b/src/dptlib/dpt20.ts @@ -0,0 +1,49 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT20: 1-byte HVAC +// +// FIXME: help needed + +const config: DatapointConfig = { + id: "dpt20", + formatAPDU: (value) => { + log.debug("./knx/src/dpt20.js : input value = " + value); + return Buffer.from([value]); + }, + + fromBuffer: (buf) => { + if (buf.length !== 1) throw "Buffer should be 1 bytes long"; + const ret = buf.readUInt8(0); + log.debug(" dpt20.js fromBuffer : " + ret); + return ret; + }, + + basetype: { + bitlength: 8, + range: [undefined, undefined], // TODO: verify + valuetype: "basic", + desc: "1-byte", + }, + + subtypes: { + // 20.102 HVAC mode + 102: { + name: "HVAC_Mode", + desc: "", + unit: "", + scalar_range: [undefined, undefined], // TODO: verify + range: [undefined, undefined], // TODO: verify + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt21.js b/src/dptlib/dpt21.js deleted file mode 100644 index 92093ec..0000000 --- a/src/dptlib/dpt21.js +++ /dev/null @@ -1,78 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -const log = require('log-driver').logger; - -// -// DPT21: 1-byte status -// -// 001 -// - OutofService b0 -// - Overridden b1 -// - Inalarm b2 -// - AlarmUnAck b3 -// - reseverd b4-7 - -// FIXME: help needed -exports.formatAPDU = function(value) { - if (value == null) return log.error('DPT21: cannot write null value'); - log.debug('./knx/src/dpt21.js : input value = ' + value); - - //var apdu_data = new Buffer(1); - //apdu_data[0] = value; - if ( typeof value === 'object' ) - return Buffer.from([(value.outofservice) + - (value.fault << 1) + - (value.overridden << 2) + - (value.inalarm << 3) + - (value.alarmeunack << 4) ]); - - log.error('DPT21: Must supply a value which is an object'); - //return apdu_data; - return Buffer.from([0]); -} - -exports.fromBuffer = function(buf) { - if (buf.length != 1) return log.error ("Buffer should be 1 bytes long"); - //if (buf.length != 1) throw "Buffer should be 1 bytes long"; - log.debug(' dpt21.js fromBuffer : ' + buf); - - //var ret = buf.readUInt8(0); - - return { - outofservice: (buf[0] & 0b00000001), - fault: (buf[0] & 0b00000010) >> 1, - overridden: (buf[0] & 0b00000100) >> 2, - inalarm: (buf[0] & 0b00001000) >> 3, - alarmunack: (buf[0] & 0b00010000) >> 4 }; - //return ret; -} - - -exports.basetype = { - "bitlength" : 8, - "range" : [ , ], - "valuetype" : "composite", - "desc" : "1-byte" -} - -exports.subtypes = { - // 21.001 status - 5 bits - "001" : { - "name" : "DPT_StatusGen", - "desc" : "General Status", - "unit" : "", - "scalar_range" : [ , ], - "range" : [ , ] - }, - // 21.002 control - 3 bits - "002" : { - "name" : "DPT_Device_Control", - "desc" : "Device Control", - "unit" : "", - "scalar_range" : [ , ], - "range" : [ , ] - } -} diff --git a/src/dptlib/dpt21.ts b/src/dptlib/dpt21.ts new file mode 100644 index 0000000..73c7499 --- /dev/null +++ b/src/dptlib/dpt21.ts @@ -0,0 +1,87 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT21: 1-byte status +// +// 001 +// - OutofService b0 +// - Overridden b1 +// - Inalarm b2 +// - AlarmUnAck b3 +// - reseverd b4-7 + +// FIXME: help needed +const config: DatapointConfig = { + id: "dpt21", + formatAPDU: function (value) { + if (value == null) return log.error("DPT21: cannot write null value"); + log.debug("./knx/src/dpt21.js : input value = " + value); + + //var apdu_data = new Buffer(1); + //apdu_data[0] = value; + if (typeof value === "object") + return Buffer.from([ + value.outofservice + + (value.fault << 1) + + (value.overridden << 2) + + (value.inalarm << 3) + + (value.alarmeunack << 4), + ]); + + log.error("DPT21: Must supply a value which is an object"); + //return apdu_data; + return Buffer.from([0]); + }, + + fromBuffer: function (buf) { + if (buf.length != 1) return log.error("Buffer should be 1 bytes long"); + //if (buf.length != 1) throw "Buffer should be 1 bytes long"; + log.debug(" dpt21.js fromBuffer : " + buf); + + //var ret = buf.readUInt8(0); + + return { + outofservice: buf[0] & 0b00000001, + fault: (buf[0] & 0b00000010) >> 1, + overridden: (buf[0] & 0b00000100) >> 2, + inalarm: (buf[0] & 0b00001000) >> 3, + alarmunack: (buf[0] & 0b00010000) >> 4, + }; + //return ret; + }, + + basetype: { + bitlength: 8, + range: [undefined, undefined], + valuetype: "composite", + desc: "1-byte", + }, + + subtypes: { + // 21.001 status - 5 bits + "001": { + name: "DPT_StatusGen", + desc: "General Status", + unit: "", + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + // 21.002 control - 3 bits + "002": { + name: "DPT_Device_Control", + desc: "Device Control", + unit: "", + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt232.js b/src/dptlib/dpt232.js deleted file mode 100644 index cb91c23..0000000 --- a/src/dptlib/dpt232.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2019 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT232: 3-byte RGB color array -// MSB: Red, Green, LSB: Blue -// -exports.formatAPDU = (value) => { - if (value == null) return log.error('DPT232: cannot write null value'); - - if (typeof value === 'object') { - const { red, green, blue } = value; - if ( - red >= 0 && - red <= 255 && - green >= 0 && - green <= 255 && - blue >= 0 && - blue <= 255 - ) - return Buffer.from([red, green, blue]); - } - log.error( - 'DPT232: Must supply an value {red:0..255, green:0.255, blue:0.255}' - ); -}; - -exports.fromBuffer = (buf) => { - const [red, green, blue] = buf; - return { red, green, blue }; -}; - -exports.basetype = { - bitlength: 3 * 8, - valuetype: 'basic', - desc: 'RGB array', -}; - -exports.subtypes = { - 600: { - name: 'RGB', - desc: 'RGB color triplet', - unit: '', - scalar_range: [,], - range: [,], - }, -}; diff --git a/src/dptlib/dpt232.ts b/src/dptlib/dpt232.ts new file mode 100644 index 0000000..7ed436e --- /dev/null +++ b/src/dptlib/dpt232.ts @@ -0,0 +1,58 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2019 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT232: 3-byte RGB color array +// MSB: Red, Green, LSB: Blue +// +const config: DatapointConfig = { + id: "dpt232", + formatAPDU: (value) => { + if (value == null) return log.error("DPT232: cannot write null value"); + + if (typeof value === "object") { + const { red, green, blue } = value; + if ( + red >= 0 && + red <= 255 && + green >= 0 && + green <= 255 && + blue >= 0 && + blue <= 255 + ) + return Buffer.from([red, green, blue]); + } + log.error( + "DPT232: Must supply an value {red:0..255, green:0.255, blue:0.255}" + ); + }, + + fromBuffer: (buf) => { + const [red, green, blue] = buf; + return { red, green, blue }; + }, + basetype: { + bitlength: 3 * 8, + valuetype: "basic", + desc: "RGB array", + }, + + subtypes: { + 600: { + name: "RGB", + desc: "RGB color triplet", + unit: "", + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt237.js b/src/dptlib/dpt237.js deleted file mode 100644 index e1163cb..0000000 --- a/src/dptlib/dpt237.js +++ /dev/null @@ -1,72 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -const log = require('log-driver').logger; - -// -// DPT237: 2-byte unsigned value -// -exports.formatAPDU = function(value) { - if (value == null) return log.error('DPT237: cannot write null value'); - - log.trace('dpt278.js : input value = ' + value); - - var apdu_data = new Buffer(2); - - //console.log("Buffer lenght: ", apdu_data.length); - if ( typeof value === 'object' && - value.hasOwnProperty('addresstype') && - value.hasOwnProperty('address') && - value.hasOwnProperty('readresponse') && - value.hasOwnProperty('lampfailure') && - value.hasOwnProperty('ballastfailure') && - value.hasOwnProperty('convertorerror') - ) { - // these are reversed as [0] is high and [1] low - apdu_data[1] = [(value.address & 0b00011111) + - (value.addresstype << 5) + - (value.readresponse << 6) + - (value.lampfailure << 7)]; - apdu_data[0] = [(value.ballastfailure & 0b00000001) + - (value.convertorerror << 1) ]; - - return apdu_data; - } - - log.error('DPT237: Must supply an value {address:[0,63] or [0,15], address type:{0,1}, ...}'); - - return apdu_data; -} - -exports.fromBuffer = function(buf) { - if (buf.length !== 2) return log.error('Buffer should be 2 byte long'); - - return { address: (buf[1] & 0b00011111), - addresstype: (buf[1] & 0b00100000) >> 5, - readresponse: (buf[1] & 0b01000000) >> 6, - lampfailure: (buf[1] & 0b10000000) >> 7, - ballastfailure: (buf[0] & 0b00000001), - convertorerror: (buf[0] & 0b00000010) >> 1 }; -} - - -exports.basetype = { - "bitlength" : 16, - "range" : [ , ], - "valuetype" : "composite", - "desc" : "2-byte" -} - -exports.subtypes = { - // 237.600 HVAC mode - "600" : { - "name" : "HVAC_Mode", - "desc" : "", - "unit" : "", - "scalar_range" : [ , ], - "range" : [ , ] - }, - -} diff --git a/src/dptlib/dpt237.ts b/src/dptlib/dpt237.ts new file mode 100644 index 0000000..c69fb3e --- /dev/null +++ b/src/dptlib/dpt237.ts @@ -0,0 +1,84 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT237: 2-byte unsigned value +// +const config: DatapointConfig = { + id: "dpt237", + formatAPDU: function (value) { + if (value == null) return log.error("DPT237: cannot write null value"); + + log.trace("dpt278.js : input value = " + value); + + var apdu_data = Buffer.alloc(2); + + //console.log("Buffer lenght: ", apdu_data.length); + if ( + typeof value === "object" && + value.hasOwnProperty("addresstype") && + value.hasOwnProperty("address") && + value.hasOwnProperty("readresponse") && + value.hasOwnProperty("lampfailure") && + value.hasOwnProperty("ballastfailure") && + value.hasOwnProperty("convertorerror") + ) { + // these are reversed as [0] is high and [1] low + apdu_data[1] = + (value.address & 0b00011111) + + (value.addresstype << 5) + + (value.readresponse << 6) + + (value.lampfailure << 7); + apdu_data[0] = + (value.ballastfailure & 0b00000001) + (value.convertorerror << 1); + + return apdu_data; + } + + log.error( + "DPT237: Must supply an value {address:[0,63] or [0,15], address type:{0,1}, ...}" + ); + + return apdu_data; + }, + + fromBuffer: function (buf) { + if (buf.length !== 2) return log.error("Buffer should be 2 byte long"); + + return { + address: buf[1] & 0b00011111, + addresstype: (buf[1] & 0b00100000) >> 5, + readresponse: (buf[1] & 0b01000000) >> 6, + lampfailure: (buf[1] & 0b10000000) >> 7, + ballastfailure: buf[0] & 0b00000001, + convertorerror: (buf[0] & 0b00000010) >> 1, + }; + }, + + basetype: { + bitlength: 16, + range: [undefined, undefined], + valuetype: "composite", + desc: "2-byte", + }, + + subtypes: { + // 237.600 HVAC mode + "600": { + name: "HVAC_Mode", + desc: "", + unit: "", + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt238.js b/src/dptlib/dpt238.js deleted file mode 100644 index 7bf2c93..0000000 --- a/src/dptlib/dpt238.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT238: 1-byte unsigned value -// -// DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) -exports.formatAPDU = (value) => { - const apdu_data = Buffer.from([value]); - log.trace( - 'dpt238.js : input value = ' + value + ' apdu_data = ' + apdu_data - ); - return apdu_data; -}; - -exports.fromBuffer = (buf) => buf[0]; - -exports.basetype = { - bitlength: 8, - range: [,], - valuetype: 'basic', - desc: '1-byte', -}; - -exports.subtypes = { - // 20.102 HVAC mode - 102: { - name: 'HVAC_Mode', - desc: '', - unit: '', - scalar_range: [,], - range: [,], - }, - - // 5.003 angle (degrees 0=0, ff=360) - '003': { - name: 'DPT_Angle', - desc: 'angle degrees', - unit: '°', - scalar_range: [0, 360], - }, - - // 5.004 percentage (0..255%) - '004': { - name: 'DPT_Percent_U8', - desc: 'percent', - unit: '%', - }, - - // 5.005 ratio (0..255) - '005': { - name: 'DPT_DecimalFactor', - desc: 'ratio', - unit: 'ratio', - }, - - // 5.006 tariff (0..255) - '006': { - name: 'DPT_Tariff', - desc: 'tariff', - unit: 'tariff', - }, - - // 5.010 counter pulses (0..255) - '010': { - name: 'DPT_Value_1_Ucount', - desc: 'counter pulses', - unit: 'pulses', - }, -}; diff --git a/src/dptlib/dpt238.ts b/src/dptlib/dpt238.ts new file mode 100644 index 0000000..42107ce --- /dev/null +++ b/src/dptlib/dpt238.ts @@ -0,0 +1,82 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT238: 1-byte unsigned value +// +// DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) +const config: DatapointConfig = { + id: "dpt238", + formatAPDU: (value) => { + const apdu_data = Buffer.from([value]); + log.trace( + "dpt238.js : input value = " + value + " apdu_data = " + apdu_data + ); + return apdu_data; + }, + + fromBuffer: (buf) => buf[0], + + basetype: { + bitlength: 8, + range: [undefined,undefined], + valuetype: "basic", + desc: "1-byte", + }, + + subtypes: { + // 20.102 HVAC mode + 102: { + name: "HVAC_Mode", + desc: "", + unit: "", + scalar_range: [undefined,undefined], + range: [undefined,undefined], + }, + + // 5.003 angle (degrees 0=0, ff=360) + "003": { + name: "DPT_Angle", + desc: "angle degrees", + unit: "°", + scalar_range: [0, 360], + }, + + // 5.004 percentage (0..255%) + "004": { + name: "DPT_Percent_U8", + desc: "percent", + unit: "%", + }, + + // 5.005 ratio (0..255) + "005": { + name: "DPT_DecimalFactor", + desc: "ratio", + unit: "ratio", + }, + + // 5.006 tariff (0..255) + "006": { + name: "DPT_Tariff", + desc: "tariff", + unit: "tariff", + }, + + // 5.010 counter pulses (0..255) + "010": { + name: "DPT_Value_1_Ucount", + desc: "counter pulses", + unit: "pulses", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt3.js b/src/dptlib/dpt3.js deleted file mode 100644 index f934745..0000000 --- a/src/dptlib/dpt3.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT3.*: 4-bit dimming/blinds control -// -exports.formatAPDU = (value) => { - if (value == null) return log.warn('DPT3: cannot write null value'); - - if ( - typeof value == 'object' && - value.hasOwnProperty('decr_incr') && - value.hasOwnProperty('data') - ) - return Buffer.from([(value.decr_incr << 3) + (value.data & 0b00000111)]); - - log.error('Must supply a value object of {decr_incr, data}'); - // FIXME: should this return zero buffer when error? Or nothing? - return Buffer.from([0]); -}; - -exports.fromBuffer = (buf) => { - if (buf.length != 1) return log.error('DPT3: Buffer should be 1 byte long'); - - return { - decr_incr: (buf[0] & 0b00001000) >> 3, - data: buf[0] & 0b00000111, - }; -}; - -exports.basetype = { - bitlength: 4, - valuetype: 'composite', - desc: '4-bit relative dimming control', -}; - -exports.subtypes = { - // 3.007 dimming control - '007': { - name: 'DPT_Control_Dimming', - desc: 'dimming control', - }, - - // 3.008 blind control - '008': { - name: 'DPT_Control_Blinds', - desc: 'blinds control', - }, -}; - -/* - 2.6.3.5 Behavior -Status -off dimming actuator switched off -on dimming actuator switched on, constant brightness, at least - minimal brightness dimming -dimming actuator switched on, moving from actual value in direction of - set value -Events - position = 0 off command - position = 1 on command - control = up dX command, dX more bright dimming - control = down dX command, dX less bright dimming - control = stop stop command - value = 0 dimming value = off - value = x% dimming value = x% (not zero) - value_reached actual value reached set value - -The step size dX for up and down dimming may be 1/1, 1/2, 1/4, 1/8, 1/16, 1/32 and 1/64 of -the full dimming range (0 - FFh). - -3.007 dimming control -3.008 blind control -*/ diff --git a/src/dptlib/dpt3.ts b/src/dptlib/dpt3.ts new file mode 100644 index 0000000..548e133 --- /dev/null +++ b/src/dptlib/dpt3.ts @@ -0,0 +1,85 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; +import KnxLog from "../KnxLog"; + +const log = KnxLog.get(); + +interface Dpt3Value { + decr_incr: number; + data: number; +} + +const config: DatapointConfig = { + id: "dpt3", + formatAPDU: (value: Dpt3Value) => { + if (value == null) return log.warn("DPT3: cannot write null value"); + + if ( + typeof value == "object" && + value.hasOwnProperty("decr_incr") && + value.hasOwnProperty("data") + ) + return Buffer.from([(value.decr_incr << 3) + (value.data & 0b00000111)]); + + log.error("Must supply a value object of {decr_incr, data}"); + // FIXME: should this return zero buffer when error? Or nothing? + return Buffer.from([0]); + }, + fromBuffer: (buf: Buffer) => { + if (buf.length != 1) return log.error("DPT3: Buffer should be 1 byte long"); + + return { + decr_incr: (buf[0] & 0b00001000) >> 3, + data: buf[0] & 0b00000111, + }; + }, + basetype: { + bitlength: 4, + valuetype: "composite", + desc: "4-bit relative dimming control", + }, + subtypes: { + // 3.007 dimming control + "007": { + name: "DPT_Control_Dimming", + desc: "dimming control", + }, + + // 3.008 blind control + "008": { + name: "DPT_Control_Blinds", + desc: "blinds control", + }, + }, +}; + +export default config; + +/* + 2.6.3.5 Behavior +Status +off dimming actuator switched off +on dimming actuator switched on, constant brightness, at least + minimal brightness dimming +dimming actuator switched on, moving from actual value in direction of + set value +Events + position = 0 off command + position = 1 on command + control = up dX command, dX more bright dimming + control = down dX command, dX less bright dimming + control = stop stop command + value = 0 dimming value = off + value = x% dimming value = x% (not zero) + value_reached actual value reached set value + +The step size dX for up and down dimming may be 1/1, 1/2, 1/4, 1/8, 1/16, 1/32 and 1/64 of +the full dimming range (0 - FFh). + +3.007 dimming control +3.008 blind control +*/ diff --git a/src/dptlib/dpt4.js b/src/dptlib/dpt4.js deleted file mode 100644 index 90b43eb..0000000 --- a/src/dptlib/dpt4.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT4: 8-bit character -// -exports.formatAPDU = (value) => { - if (value == null) return log.warn('DPT4: cannot write null value'); - - if (typeof value !== 'string') - return log.warn('DPT4: Must supply a character or string'); - - const apdu_data = value.charCodeAt(0); - if (apdu_data > 255) return log.warn('DPT4: must supply an ASCII character'); - - return Buffer.from([apdu_data]); -}; - -exports.fromBuffer = (buf) => { - if (buf.length != 1) return log.warn('DPT4: Buffer should be 1 byte long'); - - return String.fromCharCode(buf[0]); -}; - -exports.basetype = { - bitlength: 8, - valuetype: 'basic', - desc: '8-bit character', -}; - -exports.subtypes = { - // 4.001 character (ASCII) - '001': { - name: 'DPT_Char_ASCII', - desc: 'ASCII character (0-127)', - range: [0, 127], - use: 'G', - }, - // 4.002 character (ISO-8859-1) - '002': { - name: 'DPT_Char_8859_1', - desc: 'ISO-8859-1 character (0..255)', - use: 'G', - }, -}; diff --git a/src/dptlib/dpt4.ts b/src/dptlib/dpt4.ts new file mode 100644 index 0000000..c1d6906 --- /dev/null +++ b/src/dptlib/dpt4.ts @@ -0,0 +1,55 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; +import KnxLog from "../KnxLog"; + +const log = KnxLog.get(); +// +// DPT4: 8-bit character +// + +const config: DatapointConfig = { + id: "dpt4", + formatAPDU: (value: string): Buffer | void => { + if (value == null) return log.warn("DPT4: cannot write null value"); + + if (typeof value !== "string") + return log.warn("DPT4: Must supply a character or string"); + + const apdu_data: number = value.charCodeAt(0); + if (apdu_data > 255) + return log.warn("DPT4: must supply an ASCII character"); + + return Buffer.from([apdu_data]); + }, + fromBuffer: (buf: Buffer): string | void => { + if (buf.length != 1) return log.warn("DPT4: Buffer should be 1 byte long"); + + return String.fromCharCode(buf[0]); + }, + basetype: { + bitlength: 8, + valuetype: "basic", + desc: "8-bit character", + }, + subtypes: { + // 4.001 character (ASCII) + "001": { + name: "DPT_Char_ASCII", + desc: "ASCII character (0-127)", + range: [0, 127], + use: "G", + }, + // 4.002 character (ISO-8859-1) + "002": { + name: "DPT_Char_8859_1", + desc: "ISO-8859-1 character (0..255)", + use: "G", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt5.js b/src/dptlib/dpt5.js deleted file mode 100644 index 9ed990c..0000000 --- a/src/dptlib/dpt5.js +++ /dev/null @@ -1,54 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT5: 8-bit unsigned value -// -// DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) -exports.basetype = { - "bitlength" : 8, - "signedness": "unsigned", - "range" : [0, 255], - "valuetype" : "basic", - "desc" : "8-bit unsigned value" -} - -exports.subtypes = { - // 5.001 percentage (0=0..ff=100%) - "001" : { - "name" : "DPT_Scaling", "desc" : "percent", - "unit" : "%", "scalar_range" : [0, 100] - }, - - // 5.003 angle (degrees 0=0, ff=360) - "003" : { - "name" : "DPT_Angle", "desc" : "angle degrees", - "unit" : "°", "scalar_range" : [0, 360] - }, - - // 5.004 percentage (0..255%) - "004" : { - "name" : "DPT_Percent_U8", "desc" : "percent", - "unit" : "%", - }, - - // 5.005 ratio (0..255) - "005" : { - "name" : "DPT_DecimalFactor", "desc" : "ratio", - "unit" : "ratio", - }, - - // 5.006 tariff (0..255) - "006" : { - "name" : "DPT_Tariff", "desc" : "tariff", - "unit" : "tariff", - }, - - // 5.010 counter pulses (0..255) - "010" : { - "name" : "DPT_Value_1_Ucount", "desc" : "counter pulses", - "unit" : "pulses", - }, -} diff --git a/src/dptlib/dpt5.ts b/src/dptlib/dpt5.ts new file mode 100644 index 0000000..17cf8d3 --- /dev/null +++ b/src/dptlib/dpt5.ts @@ -0,0 +1,70 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// +// DPT5: 8-bit unsigned value +// +// DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) + +const config: DatapointConfig = { + id: "dpt5", + basetype: { + bitlength: 8, + signedness: "unsigned", + range: [0, 255], + valuetype: "basic", + desc: "8-bit unsigned value", + }, + + subtypes: { + // 5.001 percentage (0=0..ff=100%) + "001": { + name: "DPT_Scaling", + desc: "percent", + unit: "%", + scalar_range: [0, 100], + }, + + // 5.003 angle (degrees 0=0, ff=360) + "003": { + name: "DPT_Angle", + desc: "angle degrees", + unit: "°", + scalar_range: [0, 360], + }, + + // 5.004 percentage (0..255%) + "004": { + name: "DPT_Percent_U8", + desc: "percent", + unit: "%", + }, + + // 5.005 ratio (0..255) + "005": { + name: "DPT_DecimalFactor", + desc: "ratio", + unit: "ratio", + }, + + // 5.006 tariff (0..255) + "006": { + name: "DPT_Tariff", + desc: "tariff", + unit: "tariff", + }, + + // 5.010 counter pulses (0..255) + "010": { + name: "DPT_Value_1_Ucount", + desc: "counter pulses", + unit: "pulses", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt6.js b/src/dptlib/dpt6.js deleted file mode 100644 index c99cabf..0000000 --- a/src/dptlib/dpt6.js +++ /dev/null @@ -1,33 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// Bitstruct to parse a DPT6 frame (8-bit signed integer) -// Always 8-bit aligned. - -// DPT Basetype info -exports.basetype = { - "bitlength" : 8, - "signedness": "signed", - "valuetype" : "basic", - "desc" : "8-bit signed value", - "range" : [-128, 127] -} - -// DPT subtypes info -exports.subtypes = { - // 6.001 percentage (-128%..127%) - "001" : { - "name" : "DPT_Switch", "desc" : "percent", - "unit" : "%", - }, - - // 6.002 counter pulses (-128..127) - "010" : { - "name" : "DPT_Bool", "desc" : "counter pulses", - "unit" : "pulses" - }, - - // -} diff --git a/src/dptlib/dpt6.ts b/src/dptlib/dpt6.ts new file mode 100644 index 0000000..36c74d2 --- /dev/null +++ b/src/dptlib/dpt6.ts @@ -0,0 +1,43 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// Bitstruct to parse a DPT6 frame (8-bit signed integer) +// Always 8-bit aligned. + +// DPT Basetype info + +const config: DatapointConfig = { + id: "dpt6", + basetype: { + bitlength: 8, + signedness: "signed", + valuetype: "basic", + desc: "8-bit signed value", + range: [-128, 127], + }, + + // DPT subtypes info + subtypes: { + // 6.001 percentage (-128%..127%) + "001": { + name: "DPT_Switch", + desc: "percent", + unit: "%", + }, + + // 6.002 counter pulses (-128..127) + "010": { + name: "DPT_Bool", + desc: "counter pulses", + unit: "pulses", + }, + + // + }, +}; + +export default config; diff --git a/src/dptlib/dpt7.js b/src/dptlib/dpt7.js deleted file mode 100644 index 7c864e3..0000000 --- a/src/dptlib/dpt7.js +++ /dev/null @@ -1,101 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT7: 16-bit unsigned integer value -// -exports.basetype = { - "bitlength" : 16, - "signedness": "unsigned", - "valuetype" : "basic", - "desc" : "16-bit unsigned value" -} - -// DPT subtypes info -exports.subtypes = { - // 7.001 pulses - "001" : { "use" : "G", - "name" : "DPT_Value_2_Ucount", - "desc" : "pulses", - "unit" : "pulses" - }, - - // 7.002 time(ms) - "002" : { "use" : "G", - "name" : "DPT_TimePeriodMsec", - "desc" : "time (ms)", - "unit" : "milliseconds" - }, - - // 7.003 time (10ms) - "003" : { "use" : "G", - "name" : "DPT_TimePeriod10Msec", - "desc" : "time (10ms)", - "unit" : "centiseconds" - }, - - // 7.004 time (100ms) - "004" : { "use" : "G", - "name" : "DPT_TimePeriod100Msec", - "desc" : "time (100ms)", - "unit" : "deciseconds" - }, - - // 7.005 time (sec) - "005" : { "use" : "G", - "name" : "DPT_TimePeriodSec", - "desc" : "time (s)", - "unit" : "seconds" - }, - - // 7.006 time (min) - "006" : { "use" : "G", - "name" : "DPT_TimePeriodMin", - "desc" : "time (min)", - "unit" : "minutes" - }, - - // 7.007 time (hour) - "007" : { "use" : "G", - "name" : "DPT_TimePeriodHrs", - "desc" : "time (hrs)", - "unit" : "hours" - }, - - // 7.010 DPT_PropDataType - // not to be used in runtime communications! - "010" : { "use" : "FB", - "name" : "DPT_PropDataType", - "desc" : "Identifier Interface Object Property data type " - }, - - // 7.011 - "011" : { "use" : "FB SAB", - "name" : "DPT_Length_mm", - "desc" : "Length in mm", - "unit" : "mm" - }, - - // 7.012 - "012" : { "use" : "FB", - "name" : "DPT_UEICurrentmA", - "desc" : "bus power supply current (mA)", - "unit" : "mA" - }, - - // 7.013 - "013" : { "use" : "FB", - "name" : "DPT_Brightness", - "desc" : "interior brightness", - "unit" : "lux" - }, - - // 7.600 - "600" : { "use" : "FB", - "name" : "DPT_Absolute_Colour_Temperature", - "desc" : "Absolute colour temperature", - "unit" : "K" - } -} diff --git a/src/dptlib/dpt7.ts b/src/dptlib/dpt7.ts new file mode 100644 index 0000000..7a23804 --- /dev/null +++ b/src/dptlib/dpt7.ts @@ -0,0 +1,120 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// +// DPT7: 16-bit unsigned integer value +// +const config: DatapointConfig = { + id: "dpt7", + basetype: { + bitlength: 16, + signedness: "unsigned", + valuetype: "basic", + desc: "16-bit unsigned value", + }, + + // DPT subtypes info + subtypes: { + // 7.001 pulses + "001": { + use: "G", + name: "DPT_Value_2_Ucount", + desc: "pulses", + unit: "pulses", + }, + + // 7.002 time(ms) + "002": { + use: "G", + name: "DPT_TimePeriodMsec", + desc: "time (ms)", + unit: "milliseconds", + }, + + // 7.003 time (10ms) + "003": { + use: "G", + name: "DPT_TimePeriod10Msec", + desc: "time (10ms)", + unit: "centiseconds", + }, + + // 7.004 time (100ms) + "004": { + use: "G", + name: "DPT_TimePeriod100Msec", + desc: "time (100ms)", + unit: "deciseconds", + }, + + // 7.005 time (sec) + "005": { + use: "G", + name: "DPT_TimePeriodSec", + desc: "time (s)", + unit: "seconds", + }, + + // 7.006 time (min) + "006": { + use: "G", + name: "DPT_TimePeriodMin", + desc: "time (min)", + unit: "minutes", + }, + + // 7.007 time (hour) + "007": { + use: "G", + name: "DPT_TimePeriodHrs", + desc: "time (hrs)", + unit: "hours", + }, + + // 7.010 DPT_PropDataType + // not to be used in runtime communications! + "010": { + use: "FB", + name: "DPT_PropDataType", + desc: "Identifier Interface Object Property data type ", + }, + + // 7.011 + "011": { + use: "FB SAB", + name: "DPT_Length_mm", + desc: "Length in mm", + unit: "mm", + }, + + // 7.012 + "012": { + use: "FB", + name: "DPT_UEICurrentmA", + desc: "bus power supply current (mA)", + unit: "mA", + }, + + // 7.013 + "013": { + use: "FB", + name: "DPT_Brightness", + desc: "interior brightness", + unit: "lux", + }, + + // 7.600 + "600": { + use: "FB", + name: "DPT_Absolute_Colour_Temperature", + desc: "Absolute colour temperature", + unit: "K", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt8.js b/src/dptlib/dpt8.js deleted file mode 100644 index a6a80ab..0000000 --- a/src/dptlib/dpt8.js +++ /dev/null @@ -1,83 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -// -// DPT8.*: 2-byte signed value -// - -// DPT8 basetype info -exports.basetype = { - "bitlength" : 16, - "signedness": "signed", - "valuetype" : "basic", - "range" : [-32768, 32767], - "desc" : "16-bit signed value" -} - -// DPT8 subtypes info -exports.subtypes = { - // 8.001 pulses difference - "001" : { - "name" : "DPT_Value_2_Count", - "desc" : "pulses", - "unit" : "pulses" - }, - - // 8.002 time lag (ms) - "002" : { - "name" : "DPT_DeltaTimeMsec", - "desc" : "time lag(ms)", - "unit" : "milliseconds" - }, - - // 8.003 time lag (10ms) - "003" : { - "name" : "DPT_DeltaTime10Msec", - "desc" : "time lag(10ms)", - "unit" : "centiseconds" - }, - - // 8.004 time lag (100ms) - "004" : { - "name" : "DPT_DeltaTime100Msec", - "desc" : "time lag(100ms)", - "unit" : "deciseconds" - }, - - // 8.005 time lag (sec) - "005" : { - "name" : "DPT_DeltaTimeSec", - "desc" : "time lag(s)", - "unit" : "seconds" - }, - - // 8.006 time lag (min) - "006" : { - "name" : "DPT_DeltaTimeMin", - "desc" : "time lag(min)", - "unit" : "minutes" - }, - - // 8.007 time lag (hour) - "007" : { - "name" : "DPT_DeltaTimeHrs", - "desc" : "time lag(hrs)", - "unit" : "hours" - }, - - // 8.010 percentage difference (%) - "010" : { - "name" : "DPT_Percent_V16", - "desc" : "percentage difference", - "unit" : "%" - }, - - // 8.011 rotation angle (deg) - "011" : { - "name" : "DPT_RotationAngle", - "desc" : "angle(degrees)", - "unit" : "°" - }, -} diff --git a/src/dptlib/dpt8.ts b/src/dptlib/dpt8.ts new file mode 100644 index 0000000..36585b7 --- /dev/null +++ b/src/dptlib/dpt8.ts @@ -0,0 +1,90 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from "."; + +// +// DPT8.*: 2-byte signed value +// + +const config: DatapointConfig = { + id: "dpt8", + // DPT8 basetype info + basetype: { + bitlength: 16, + signedness: "signed", + valuetype: "basic", + range: [-32768, 32767], + desc: "16-bit signed value", + }, + + // DPT8 subtypes info + subtypes: { + // 8.001 pulses difference + "001": { + name: "DPT_Value_2_Count", + desc: "pulses", + unit: "pulses", + }, + + // 8.002 time lag (ms) + "002": { + name: "DPT_DeltaTimeMsec", + desc: "time lag(ms)", + unit: "milliseconds", + }, + + // 8.003 time lag (10ms) + "003": { + name: "DPT_DeltaTime10Msec", + desc: "time lag(10ms)", + unit: "centiseconds", + }, + + // 8.004 time lag (100ms) + "004": { + name: "DPT_DeltaTime100Msec", + desc: "time lag(100ms)", + unit: "deciseconds", + }, + + // 8.005 time lag (sec) + "005": { + name: "DPT_DeltaTimeSec", + desc: "time lag(s)", + unit: "seconds", + }, + + // 8.006 time lag (min) + "006": { + name: "DPT_DeltaTimeMin", + desc: "time lag(min)", + unit: "minutes", + }, + + // 8.007 time lag (hour) + "007": { + name: "DPT_DeltaTimeHrs", + desc: "time lag(hrs)", + unit: "hours", + }, + + // 8.010 percentage difference (%) + "010": { + name: "DPT_Percent_V16", + desc: "percentage difference", + unit: "%", + }, + + // 8.011 rotation angle (deg) + "011": { + name: "DPT_RotationAngle", + desc: "angle(degrees)", + unit: "°", + }, + }, +}; + +export default config; diff --git a/src/dptlib/dpt9.js b/src/dptlib/dpt9.js deleted file mode 100644 index d130341..0000000 --- a/src/dptlib/dpt9.js +++ /dev/null @@ -1,229 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -const log = require('log-driver').logger; - -// -// DPT9.*: 2-byte floating point value -// - -const util = require('util'); -// kudos to http://croquetweak.blogspot.gr/2014/08/deconstructing-floats-frexp-and-ldexp.html -const ldexp = (mantissa, exponent) => - exponent > 1023 // avoid multiplying by infinity - ? mantissa * Math.pow(2, 1023) * Math.pow(2, exponent - 1023) - : exponent < -1074 // avoid multiplying by zero - ? mantissa * Math.pow(2, -1074) * Math.pow(2, exponent + 1074) - : mantissa * Math.pow(2, exponent); - -const frexp = (value) => { - if (value === 0) return [0, 0]; - const data = new DataView(new ArrayBuffer(8)); - data.setFloat64(0, value); - let bits = (data.getUint32(0) >>> 20) & 0x7ff; - if (bits === 0) { - data.setFloat64(0, value * Math.pow(2, 64)); - bits = ((data.getUint32(0) >>> 20) & 0x7ff) - 64; - } - const exponent = bits - 1022; - const mantissa = ldexp(value, -exponent); - return [mantissa, exponent]; -}; - -exports.formatAPDU = (value) => { - if (!isFinite(value)) - return log.warn('DPT9: cannot write non-numeric or undefined value'); - - const arr = frexp(value); - const [mantissa, exponent] = arr; - // find the minimum exponent that will upsize the normalized mantissa (0,5 to 1 range) - // in order to fit in 11 bits ([-2048, 2047]) - let max_mantissa = 0; - for (e = exponent; e >= -15; e--) { - max_mantissa = ldexp(100 * mantissa, e); - if (max_mantissa > -2048 && max_mantissa < 2047) break; - } - const sign = mantissa < 0 ? 1 : 0; - const mant = mantissa < 0 ? ~(max_mantissa ^ 2047) : max_mantissa; - const exp = exponent - e; - // yucks - return Buffer.from([(sign << 7) + (exp << 3) + (mant >> 8), mant % 256]); -}; - -exports.fromBuffer = (buf) => { - if (buf.length != 2) - return log.warn( - 'DPT9.fromBuffer: buf should be 2 bytes long (got %d bytes)', - buf.length - ); - - const sign = buf[0] >> 7; - const exponent = (buf[0] & 0b01111000) >> 3; - let mantissa = 256 * (buf[0] & 0b00000111) + buf[1]; - if (sign) mantissa = ~(mantissa ^ 2047); - return parseFloat(ldexp(0.01 * mantissa, exponent).toPrecision(15)); -}; - -// DPT9 basetype info -exports.basetype = { - bitlength: 16, - valuetype: 'basic', - desc: '16-bit floating point value', -}; - -// DPT9 subtypes -exports.subtypes = { - // 9.001 temperature (oC) - '001': { - name: 'DPT_Value_Temp', - desc: 'temperature', - unit: '°C', - range: [-273, 670760], - }, - - // 9.002 temperature difference (oC) - '002': { - name: 'DPT_Value_Tempd', - desc: 'temperature difference', - unit: '°C', - range: [-670760, 670760], - }, - - // 9.003 kelvin/hour (K/h) - '003': { - name: 'DPT_Value_Tempa', - desc: 'kelvin/hour', - unit: '°K/h', - range: [-670760, 670760], - }, - - // 9.004 lux (Lux) - '004': { - name: 'DPT_Value_Lux', - desc: 'lux', - unit: 'lux', - range: [0, 670760], - }, - - // 9.005 speed (m/s) - '005': { - name: 'DPT_Value_Wsp', - desc: 'wind speed', - unit: 'm/s', - range: [0, 670760], - }, - - // 9.006 pressure (Pa) - '006': { - name: 'DPT_Value_Pres', - desc: 'pressure', - unit: 'Pa', - range: [0, 670760], - }, - - // 9.007 humidity (%) - '007': { - name: 'DPT_Value_Humidity', - desc: 'humidity', - unit: '%', - range: [0, 670760], - }, - - // 9.008 parts/million (ppm) - '008': { - name: 'DPT_Value_AirQuality', - desc: 'air quality', - unit: 'ppm', - range: [0, 670760], - }, - - // 9.010 time (s) - '010': { - name: 'DPT_Value_Time1', - desc: 'time(sec)', - unit: 's', - range: [-670760, 670760], - }, - - // 9.011 time (ms) - '011': { - name: 'DPT_Value_Time2', - desc: 'time(msec)', - unit: 'ms', - range: [-670760, 670760], - }, - - // 9.020 voltage (mV) - '020': { - name: 'DPT_Value_Volt', - desc: 'voltage', - unit: 'mV', - range: [-670760, 670760], - }, - - // 9.021 current (mA) - '021': { - name: 'DPT_Value_Curr', - desc: 'current', - unit: 'mA', - range: [-670760, 670760], - }, - - // 9.022 power density (W/m2) - '022': { - name: 'DPT_PowerDensity', - desc: 'power density', - unit: 'W/m²', - range: [-670760, 670760], - }, - - // 9.023 kelvin/percent (K/%) - '023': { - name: 'DPT_KelvinPerPercent', - desc: 'Kelvin / %', - unit: 'K/%', - range: [-670760, 670760], - }, - - // 9.024 power (kW) - '024': { - name: 'DPT_Power', - desc: 'power (kW)', - unit: 'kW', - range: [-670760, 670760], - }, - - // 9.025 volume flow (l/h) - '025': { - name: 'DPT_Value_Volume_Flow', - desc: 'volume flow', - unit: 'l/h', - range: [-670760, 670760], - }, - - // 9.026 rain amount (l/m2) - '026': { - name: 'DPT_Rain_Amount', - desc: 'rain amount', - unit: 'l/m²', - range: [-670760, 670760], - }, - - // 9.027 temperature (Fahrenheit) - '027': { - name: 'DPT_Value_Temp_F', - desc: 'temperature (F)', - unit: '°F', - range: -[459.6, 670760], - }, - - // 9.028 wind speed (km/h) - '028': { - name: 'DPT_Value_Wsp_kmh', - desc: 'wind speed (km/h)', - unit: 'km/h', - range: [0, 670760], - }, -}; diff --git a/src/dptlib/dpt9.ts b/src/dptlib/dpt9.ts new file mode 100644 index 0000000..89cc8d3 --- /dev/null +++ b/src/dptlib/dpt9.ts @@ -0,0 +1,237 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import logger from "../KnxLog"; +import { DatapointConfig } from "."; + +const log = logger.get(); + +// +// DPT9.*: 2-byte floating point value +// + +// kudos to http://croquetweak.blogspot.gr/2014/08/deconstructing-floats-frexp-and-ldexp.html +const ldexp = (mantissa, exponent) => + exponent > 1023 // avoid multiplying by infinity + ? mantissa * Math.pow(2, 1023) * Math.pow(2, exponent - 1023) + : exponent < -1074 // avoid multiplying by zero + ? mantissa * Math.pow(2, -1074) * Math.pow(2, exponent + 1074) + : mantissa * Math.pow(2, exponent); + +const frexp = (value) => { + if (value === 0) return [0, 0]; + const data = new DataView(new ArrayBuffer(8)); + data.setFloat64(0, value); + let bits = (data.getUint32(0) >>> 20) & 0x7ff; + if (bits === 0) { + data.setFloat64(0, value * Math.pow(2, 64)); + bits = ((data.getUint32(0) >>> 20) & 0x7ff) - 64; + } + const exponent = bits - 1022; + const mantissa = ldexp(value, -exponent); + return [mantissa, exponent]; +}; + +const config: DatapointConfig = { + id: "dpt9", + formatAPDU: (value) => { + if (!isFinite(value)) + return log.warn("DPT9: cannot write non-numeric or undefined value"); + + const arr = frexp(value); + const [mantissa, exponent] = arr; + // find the minimum exponent that will upsize the normalized mantissa (0,5 to 1 range) + // in order to fit in 11 bits ([-2048, 2047]) + let max_mantissa = 0; + let e: number + for (e = exponent; e >= -15; e--) { + max_mantissa = ldexp(100 * mantissa, e); + if (max_mantissa > -2048 && max_mantissa < 2047) break; + } + const sign = mantissa < 0 ? 1 : 0; + const mant = mantissa < 0 ? ~(max_mantissa ^ 2047) : max_mantissa; + const exp = exponent - e; + // yucks + return Buffer.from([(sign << 7) + (exp << 3) + (mant >> 8), mant % 256]); + }, + + fromBuffer: (buf) => { + if (buf.length != 2) + return log.warn( + "DPT9.fromBuffer: buf should be 2 bytes long (got %d bytes)", + buf.length + ); + + const sign = buf[0] >> 7; + const exponent = (buf[0] & 0b01111000) >> 3; + let mantissa = 256 * (buf[0] & 0b00000111) + buf[1]; + if (sign) mantissa = ~(mantissa ^ 2047); + return parseFloat(ldexp(0.01 * mantissa, exponent).toPrecision(15)); + }, + + // DPT9 basetype info + basetype: { + bitlength: 16, + valuetype: "basic", + desc: "16-bit floating point value", + }, + + // DPT9 subtypes + subtypes: { + // 9.001 temperature (oC) + "001": { + name: "DPT_Value_Temp", + desc: "temperature", + unit: "°C", + range: [-273, 670760], + }, + + // 9.002 temperature difference (oC) + "002": { + name: "DPT_Value_Tempd", + desc: "temperature difference", + unit: "°C", + range: [-670760, 670760], + }, + + // 9.003 kelvin/hour (K/h) + "003": { + name: "DPT_Value_Tempa", + desc: "kelvin/hour", + unit: "°K/h", + range: [-670760, 670760], + }, + + // 9.004 lux (Lux) + "004": { + name: "DPT_Value_Lux", + desc: "lux", + unit: "lux", + range: [0, 670760], + }, + + // 9.005 speed (m/s) + "005": { + name: "DPT_Value_Wsp", + desc: "wind speed", + unit: "m/s", + range: [0, 670760], + }, + + // 9.006 pressure (Pa) + "006": { + name: "DPT_Value_Pres", + desc: "pressure", + unit: "Pa", + range: [0, 670760], + }, + + // 9.007 humidity (%) + "007": { + name: "DPT_Value_Humidity", + desc: "humidity", + unit: "%", + range: [0, 670760], + }, + + // 9.008 parts/million (ppm) + "008": { + name: "DPT_Value_AirQuality", + desc: "air quality", + unit: "ppm", + range: [0, 670760], + }, + + // 9.010 time (s) + "010": { + name: "DPT_Value_Time1", + desc: "time(sec)", + unit: "s", + range: [-670760, 670760], + }, + + // 9.011 time (ms) + "011": { + name: "DPT_Value_Time2", + desc: "time(msec)", + unit: "ms", + range: [-670760, 670760], + }, + + // 9.020 voltage (mV) + "020": { + name: "DPT_Value_Volt", + desc: "voltage", + unit: "mV", + range: [-670760, 670760], + }, + + // 9.021 current (mA) + "021": { + name: "DPT_Value_Curr", + desc: "current", + unit: "mA", + range: [-670760, 670760], + }, + + // 9.022 power density (W/m2) + "022": { + name: "DPT_PowerDensity", + desc: "power density", + unit: "W/m²", + range: [-670760, 670760], + }, + + // 9.023 kelvin/percent (K/%) + "023": { + name: "DPT_KelvinPerPercent", + desc: "Kelvin / %", + unit: "K/%", + range: [-670760, 670760], + }, + + // 9.024 power (kW) + "024": { + name: "DPT_Power", + desc: "power (kW)", + unit: "kW", + range: [-670760, 670760], + }, + + // 9.025 volume flow (l/h) + "025": { + name: "DPT_Value_Volume_Flow", + desc: "volume flow", + unit: "l/h", + range: [-670760, 670760], + }, + + // 9.026 rain amount (l/m2) + "026": { + name: "DPT_Rain_Amount", + desc: "rain amount", + unit: "l/m²", + range: [-670760, 670760], + }, + + // 9.027 temperature (Fahrenheit) + "027": { + name: "DPT_Value_Temp_F", + desc: "temperature (F)", + unit: "°F", + range: [-459.6, 670760], + }, + + // 9.028 wind speed (km/h) + "028": { + name: "DPT_Value_Wsp_kmh", + desc: "wind speed (km/h)", + unit: "km/h", + range: [0, 670760], + }, + }, +}; + +export default config; diff --git a/src/dptlib/index.js b/src/dptlib/index.js deleted file mode 100644 index 2181a5a..0000000 --- a/src/dptlib/index.js +++ /dev/null @@ -1,203 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -/* -Datatypes -========= -KNX/EIB Function Information length EIS DPT Value -Switch 1 Bit EIS 1 DPT 1 0,1 -Dimming (Position, Control, Value) 1 Bit, 4 Bit, 8 Bit EIS 2 DPT 3 [0,0]...[1,7] -Time 3 Byte EIS 3 DPT 10 -Date 3 Byte EIS 4 DPT 11 -Floating point 2 Byte EIS 5 DPT 9 -671088,64 - 670760,96 -8-bit unsigned value 1 Byte EIS 6 DPT 5 0...255 -8-bit unsigned value 1 Byte DPT 5.001 DPT 5.001 0...100 -Blinds / Roller shutter 1 Bit EIS 7 DPT 1 0,1 -Priority 2 Bit EIS 8 DPT 2 [0,0]...[1,1] -IEEE Floating point 4 Byte EIS 9 DPT 14 4-Octet Float Value IEEE 754 -16-bit unsigned value 2 Byte EIS 10 DPT 7 0...65535 -16-bit signed value 2 Byte DPT 8 DPT 8 -32768...32767 -32-bit unsigned value 4 Byte EIS 11 DPT 12 0...4294967295 -32-bit signed value 4 Byte DPT 13 DPT 13 -2147483648...2147483647 -Access control 1 Byte EIS 12 DPT 15 -ASCII character 1 Byte EIS 13 DPT 4 -8859_1 character 1 Byte DPT 4.002 DPT 4.002 -8-bit signed value 1 Byte EIS 14 DPT 6 -128...127 -14 character ASCII 14 Byte EIS 15 DPT 16 -14 character 8859_1 14 Byte DPT 16.001 DPT 16.001 -Scene 1 Byte DPT 17 DPT 17 0...63 -HVAC 1 Byte DPT 20 DPT 20 0..255 -Unlimited string 8859_1 . DPT 24 DPT 24 -List 3-byte value 3 Byte DPT 232 DPT 232 RGB[0,0,0]...[255,255,255] -*/ - -const fs = require('fs'); -const path = require('path'); -const util = require('util'); -const log = require('log-driver').logger; - -const dpts = {}; -for (const entry of fs.readdirSync(__dirname)) { - const matches = entry.match(/(dpt.*)\.js/); - if (!matches) continue; - const dptid = matches[1].toUpperCase(); // DPT1..DPTxxx - const mod = require(__dirname + path.sep + entry); - if ( - !mod.hasOwnProperty('basetype') || - !mod.basetype.hasOwnProperty('bitlength') - ) - throw 'incomplete ' + dptid + ', missing basetype and/or bitlength!'; - mod.id = dptid; - dpts[dptid] = mod; - //log.trace('DPT library: loaded %s (%s)', dptid, dpts[dptid].basetype.desc); -} - -// a generic DPT resolution function -// DPTs might come in as 9/"9"/"9.001"/"DPT9.001" -dpts.resolve = (dptid) => { - const m = dptid - .toString() - .toUpperCase() - .match(/^(?:DPT)?(\d+)(\.(\d+))?$/); - if (m === null) throw 'Invalid DPT format: ' + dptid; - - const dpt = dpts[util.format('DPT%s', m[1])]; - if (!dpt) throw 'Unsupported DPT: ' + dptid; - - const cloned_dpt = cloneDpt(dpt); - if (m[3]) { - cloned_dpt.subtypeid = m[3]; - cloned_dpt.subtype = cloned_dpt.subtypes[m[3]]; - } - - return cloned_dpt; -}; - -/* POPULATE an APDU object from a given Javascript value for the given DPT - * - either by a custom DPT formatAPDU function - * - or by this generic version, which: - * -- 1) checks if the value adheres to the range set from the DPT's bitlength - * - */ -dpts.populateAPDU = (value, apdu, dptid) => { - const dpt = dpts.resolve(dptid || 'DPT1'); - const nbytes = Math.ceil(dpt.basetype.bitlength / 8); - apdu.data = Buffer.alloc(nbytes); - apdu.bitlength = (dpt.basetype && dpt.basetype.bitlength) || 1; - let tgtvalue = value; - // get the raw APDU data for the given JS value - if (typeof dpt.formatAPDU == 'function') { - // nothing to do here, DPT-specific formatAPDU implementation will handle everything - // log.trace('>>> custom formatAPDU(%s): %j', dptid, value); - apdu.data = dpt.formatAPDU(value); - // log.trace('<<< custom formatAPDU(%s): %j', dptid, apdu.data); - return apdu; - } - - if (!isFinite(value)) - throw util.format('Invalid value, expected a %s', dpt.desc); - // check if value is in range, be it explicitly defined or implied from bitlength - const [r_min, r_max] = dpt.basetype.hasOwnProperty('range') - ? dpt.basetype.range - : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; // TODO: Maybe bitshift instead of pow? - // is there a scalar range? eg. DPT5.003 angle degrees (0=0, ff=360) - if ( - dpt.hasOwnProperty('subtype') && - dpt.subtype.hasOwnProperty('scalar_range') - ) { - const [s_min, s_max] = dpt.subtype.scalar_range; - if (value < s_min || value > s_max) { - log.trace( - 'Value %j(%s) out of scalar range(%j) for %s', - value, - typeof value, - scalar, - dpt.id - ); - } else { - // convert value from its scalar representation - // e.g. in DPT5.001, 50(%) => 0x7F , 100(%) => 0xFF - const a = (s_max - s_min) / (r_max - r_min); - const b = s_min - r_min; - tgtvalue = Math.round((value - b) / a); - } - } - // just a plain numeric value, only check if within bounds - else if (value < r_min || value > r_max) { - log.trace( - 'Value %j(%s) out of bounds(%j) for %s.%s', - value, - typeof value, - range, - dpt.id, - dpt.subtypeid - ); - } - - // generic APDU is assumed to convey an unsigned integer of arbitrary bitlength - if ( - dpt.basetype.hasOwnProperty('signedness') && - dpt.basetype.signedness == 'signed' - ) { - apdu.data.writeIntBE(tgtvalue, 0, nbytes); - } else { - apdu.data.writeUIntBE(tgtvalue, 0, nbytes); - } - - // log.trace('generic populateAPDU tgtvalue=%j(%s) nbytes=%d => apdu=%j', tgtvalue, typeof tgtvalue, nbytes, apdu); -}; - -/* get the correct Javascript value from a APDU buffer for the given DPT - * - either by a custom DPT formatAPDU function - * - or by this generic version, which: - * -- 1) checks if the value adheres to the range set from the DPT's bitlength - */ -dpts.fromBuffer = (buf, dpt) => { - // sanity check - if (!dpt) throw util.format('DPT %s not found', dpt); - // get the raw APDU data for the given JS value - if (typeof dpt.fromBuffer == 'function') { - // nothing to do here, DPT-specific fromBuffer implementation will handle everything - return dpt.fromBuffer(buf); - } - // log.trace('%s buflength == %d => %j', typeof buf, buf.length, JSON.stringify(buf) ); - if (buf.length > 6) { - throw 'cannot handle unsigned integers more then 6 bytes in length'; - } - let value = 0; - if ( - dpt.basetype.hasOwnProperty('signedness') && - dpt.basetype.signedness == 'signed' - ) - value = buf.readIntBE(0, buf.length); - else value = buf.readUIntBE(0, buf.length); - - // log.trace(' ../knx/src/index.js : DPT : ' + JSON.stringify(dpt)); // for exploring dpt and implementing description - if ( - dpt.hasOwnProperty('subtype') && - dpt.subtype.hasOwnProperty('scalar_range') - ) { - const [r_min, r_max] = dpt.basetype.hasOwnProperty('range') - ? dpt.basetype.range - : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; - const [s_min, s_max] = dpt.subtype.scalar_range; - // convert value from its scalar representation - // e.g. in DPT5.001, 50(%) => 0x7F , 100(%) => 0xFF - const a = (s_max - s_min) / (r_max - r_min); - const b = s_min - r_min; - value = Math.round(a * value + b); - //log.trace('fromBuffer scalar a=%j b=%j %j', a,b, value); - } - - // log.trace('generic fromBuffer buf=%j, value=%j', buf, value); - return value; -}; - -const cloneDpt = (d) => { - const { fromBuffer, formatAPDU } = d; - return { ...JSON.parse(JSON.stringify(d)), fromBuffer, formatAPDU }; -}; - -module.exports = dpts; diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts new file mode 100644 index 0000000..547b411 --- /dev/null +++ b/src/dptlib/index.ts @@ -0,0 +1,244 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +/* +Datatypes +========= +KNX/EIB Function Information length EIS DPT Value +Switch 1 Bit EIS 1 DPT 1 0,1 +Dimming (Position, Control, Value) 1 Bit, 4 Bit, 8 Bit EIS 2 DPT 3 [0,0]...[1,7] +Time 3 Byte EIS 3 DPT 10 +Date 3 Byte EIS 4 DPT 11 +Floating point 2 Byte EIS 5 DPT 9 -671088,64 - 670760,96 +8-bit unsigned value 1 Byte EIS 6 DPT 5 0...255 +8-bit unsigned value 1 Byte DPT 5.001 DPT 5.001 0...100 +Blinds / Roller shutter 1 Bit EIS 7 DPT 1 0,1 +Priority 2 Bit EIS 8 DPT 2 [0,0]...[1,1] +IEEE Floating point 4 Byte EIS 9 DPT 14 4-Octet Float Value IEEE 754 +16-bit unsigned value 2 Byte EIS 10 DPT 7 0...65535 +16-bit signed value 2 Byte DPT 8 DPT 8 -32768...32767 +32-bit unsigned value 4 Byte EIS 11 DPT 12 0...4294967295 +32-bit signed value 4 Byte DPT 13 DPT 13 -2147483648...2147483647 +Access control 1 Byte EIS 12 DPT 15 +ASCII character 1 Byte EIS 13 DPT 4 +8859_1 character 1 Byte DPT 4.002 DPT 4.002 +8-bit signed value 1 Byte EIS 14 DPT 6 -128...127 +14 character ASCII 14 Byte EIS 15 DPT 16 +14 character 8859_1 14 Byte DPT 16.001 DPT 16.001 +Scene 1 Byte DPT 17 DPT 17 0...63 +HVAC 1 Byte DPT 20 DPT 20 0..255 +Unlimited string 8859_1 . DPT 24 DPT 24 +List 3-byte value 3 Byte DPT 232 DPT 232 RGB[0,0,0]...[255,255,255] +*/ + +import * as fs from "fs"; +import * as path from "path"; +import * as util from "util"; +import KnxLog from "../KnxLog"; + +import DPT1 from "./dpt1"; +import DPT2 from "./dpt2"; +import DPT3 from "./dpt3"; +import DPT4 from "./dpt4"; +import DPT5 from "./dpt5"; +import DPT6 from "./dpt6"; +import DPT7 from "./dpt7"; +import DPT8 from "./dpt8"; +import DPT9 from "./dpt9"; +import DPT10 from "./dpt10"; +import DPT11 from "./dpt11"; +import DPT12 from "./dpt12"; +import DPT13 from "./dpt13"; +import DPT14 from "./dpt14"; +import DPT15 from "./dpt15"; +import DPT16 from "./dpt16"; +import DPT17 from "./dpt17"; +import DPT18 from "./dpt18"; +import DPT19 from "./dpt19"; +import DPT20 from "./dpt20"; +import DPT21 from "./dpt21"; +import DPT232 from "./dpt232"; +import DPT237 from "./dpt237"; +import DPT238 from "./dpt238"; +import { Datagram } from "src/FSM"; + +const log = KnxLog.get(); + +interface DatapointSubtype { + scalar_range?: [number, number]; + name: string; + use?: string; + desc: string; + force_encoding?: string; + unit?: string; + enc?: Record; + range?: [number, number] | [undefined, undefined]; +} + +export interface DatapointConfig { + id: string; + subtypeid?: string; + basetype: { + bitlength: number; + signedness?: string; + range?: [number, number]; + valuetype: string; + desc?: string; + }; + subtype?: DatapointSubtype; + subtypes?: Record; + formatAPDU?: (value: any) => Buffer | void; + fromBuffer?: (buf: Buffer) => any; +} + +const dpts: Record = { + [DPT1.id]: DPT1, + [DPT2.id]: DPT2, + [DPT3.id]: DPT3, + [DPT4.id]: DPT4, + [DPT5.id]: DPT5, + [DPT6.id]: DPT6, + [DPT7.id]: DPT7, + [DPT8.id]: DPT8, + [DPT9.id]: DPT9, + [DPT10.id]: DPT10, + [DPT11.id]: DPT11, + [DPT12.id]: DPT12, + [DPT13.id]: DPT13, + [DPT14.id]: DPT14, + [DPT15.id]: DPT15, + [DPT16.id]: DPT16, + [DPT17.id]: DPT17, + [DPT18.id]: DPT18, + [DPT19.id]: DPT19, + [DPT20.id]: DPT20, + [DPT21.id]: DPT21, + [DPT232.id]: DPT232, + [DPT237.id]: DPT237, + [DPT238.id]: DPT238, +}; + +export function resolve(dptid: string | number) { + const m = dptid + .toString() + .toUpperCase() + .match(/^(?:DPT)?(\d+)(\.(\d+))?$/); + if (m === null) throw "Invalid DPT format: " + dptid; + + const dpt = dpts[util.format("DPT%s", m[1])]; + if (!dpt) throw "Unsupported DPT: " + dptid; + + const cloned_dpt = cloneDpt(dpt); + if (m[3]) { + cloned_dpt.subtypeid = m[3]; + cloned_dpt.subtype = cloned_dpt.subtypes[m[3]]; + } + + return cloned_dpt; +} + +export function populateAPDU( + value: any, + apdu: Datagram["cemi"]["apdu"], + dptid?: number | string +) { + const dpt = resolve(dptid || "DPT1"); + const nbytes = Math.ceil(dpt.basetype.bitlength / 8); + apdu.data = Buffer.alloc(nbytes); + apdu.bitlength = (dpt.basetype && dpt.basetype.bitlength) || 1; + let tgtvalue = value; + + if (typeof dpt.formatAPDU == "function") { + apdu.data = dpt.formatAPDU(value); + return apdu; + } + + if (!isFinite(value)) + throw util.format("Invalid value, expected a %s", dpt.basetype.desc); + + const [r_min, r_max] = dpt.basetype.hasOwnProperty("range") + ? dpt.basetype.range + : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; + + if ( + dpt.hasOwnProperty("subtype") && + dpt.subtype.hasOwnProperty("scalar_range") + ) { + const [s_min, s_max] = dpt.subtype.scalar_range; + if (value < s_min || value > s_max) { + log.trace( + "Value %j(%s) out of scalar range(%j) for %s", + value, + typeof value, + dpt.subtype.scalar_range, + dpt.id + ); + } else { + const a = (s_max - s_min) / (r_max - r_min); + const b = s_min - r_min; + tgtvalue = Math.round((value - b) / a); + } + } else if (value < r_min || value > r_max) { + log.trace( + "Value %j(%s) out of bounds(%j) for %s.%s", + value, + typeof value, + dpt.subtype.scalar_range, + dpt.id, + dpt.subtypeid + ); + } + + if ( + dpt.basetype.hasOwnProperty("signedness") && + dpt.basetype.signedness == "signed" + ) { + apdu.data.writeIntBE(tgtvalue, 0, nbytes); + } else { + apdu.data.writeUIntBE(tgtvalue, 0, nbytes); + } +} + +export function fromBuffer(buf: Buffer, dpt: DatapointConfig) { + if (!dpt) throw util.format("DPT %s not found", dpt); + + if (typeof dpt.fromBuffer == "function") { + return dpt.fromBuffer(buf); + } + + if (buf.length > 6) { + throw "cannot handle unsigned integers more then 6 bytes in length"; + } + + let value = 0; + if ( + dpt.basetype.hasOwnProperty("signedness") && + dpt.basetype.signedness == "signed" + ) + value = buf.readIntBE(0, buf.length); + else value = buf.readUIntBE(0, buf.length); + + if ( + dpt.hasOwnProperty("subtype") && + dpt.subtype.hasOwnProperty("scalar_range") + ) { + const [r_min, r_max] = dpt.basetype.hasOwnProperty("range") + ? dpt.basetype.range + : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; + const [s_min, s_max] = dpt.subtype.scalar_range; + const a = (s_max - s_min) / (r_max - r_min); + const b = s_min - r_min; + value = Math.round(a * value + b); + } + + return value; +} + +const cloneDpt = (d: DatapointConfig) => { + const { fromBuffer, formatAPDU } = d; + return { ...JSON.parse(JSON.stringify(d)), fromBuffer, formatAPDU }; +}; + +export default dpts; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..30c5cf8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,19 @@ +/** +* knx.ts - a KNX protocol stack in pure Typescript +* (C) 2016-2017 Elias Karakoulakis +*/ + +import { format } from 'util'; +import log from 'log-driver'; + +import Connection from './Connection'; +import Datapoint from './Datapoint'; +import Devices from './devices'; +import Log from './KnxLog'; + +const pkgJson = require('../package.json'); + +log.info(format('Loading %s: %s, version: %s', +pkgJson.name, pkgJson.description, pkgJson.version)); + +export { Connection, Datapoint, Devices, Log }; \ No newline at end of file diff --git a/src/types/binary-protocol.d.ts b/src/types/binary-protocol.d.ts new file mode 100644 index 0000000..637bd84 --- /dev/null +++ b/src/types/binary-protocol.d.ts @@ -0,0 +1,10 @@ +declare module "binary-protocol" { + +interface ProtocolConfig { + read(propertyName: string): void + write(value: any): void +} + export class BinaryProtocol { + define(name: string, config: ProtocolConfig): this + } +} diff --git a/src/types/machina.d.ts b/src/types/machina.d.ts new file mode 100644 index 0000000..b451892 --- /dev/null +++ b/src/types/machina.d.ts @@ -0,0 +1,40 @@ +import EventEmitter from "events"; + +declare module "machina" { + export interface States { + /** It's the "catch-all" handler which, if provided, will match any input in that state that's not explicitly matched by name */ + "*"?: () => void; + + _onEnter?: () => void; + + timeout?: string; + + _reset?: string; + + _onExit?: () => void; + + [key: string]: { + [key: string]: () => void; + }; + } + + export interface Options { + initialState: string; + states: Record; + namespace?: string; + initialize?: () => void; + } + + export class BehavioralFsm extends EventEmitter { + initialState: string; + namespace: string; + states: any// Record; + + constructor(options: Options); + static extend(protoProps: T, staticProps?: any): this; + } + + export class FSM extends BehavioralFsm { + constructor(options: Options); + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..7c9b76b --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "build"], + } + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0337e72 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./build", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "preserveSymlinks": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "typeRoots": [ + "node_modules/@types", + "src/types" + ], + "types": [ + "node" + ], + }, + "include": [ + "src", + "test", + "manualtest" + ], + "exclude": [ + "typescript-sample", + ] +} \ No newline at end of file From 164fb0196599ee6bba0e0ab0878d75dbe9aa0a6c Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 09:51:30 +0200 Subject: [PATCH 02/36] fix: merge Connection into FSM for better types --- src/Connection.ts | 434 ------------------------------- src/FSM.ts | 451 +++++++++++++++++++++++++++++++-- src/IpRoutingConnection.ts | 6 +- src/IpTunnelingConnection.ts | 38 +-- src/KnxProtocol.ts | 1 + src/index.ts | 2 +- src/types/binary-protocol.d.ts | 51 +++- src/types/machina.d.ts | 20 +- 8 files changed, 520 insertions(+), 483 deletions(-) delete mode 100644 src/Connection.ts diff --git a/src/Connection.ts b/src/Connection.ts deleted file mode 100644 index e98d444..0000000 --- a/src/Connection.ts +++ /dev/null @@ -1,434 +0,0 @@ -/** - * knx.js - a KNX protocol stack in pure Javascript - * (C) 2016-2018 Elias Karakoulakis - */ - -import * as util from "util"; - -import KnxFSM, { Datagram } from "./FSM"; -import { populateAPDU } from "./dptlib"; -import KnxLog from "./KnxLog"; -import { keyText, KnxConstants } from "./KnxConstants"; -import KnxNetProtocol from "./KnxProtocol"; - -// bind incoming UDP packet handler -KnxFSM.prototype.onUdpSocketMessage = function ( - msg: Buffer, - rinfo: any, - callback: () => void -): void { - // get the incoming packet's service type ... - try { - const reader = KnxNetProtocol.createReader(msg); - reader.KNXNetHeader("tmp"); - const dg = reader.next()["tmp"]; - const descr = datagramDesc(dg); - KnxLog.get().trace( - "(%s): Received %s message: %j", - this.compositeState(), - descr, - dg - ); - if ( - !isNaN(this.channel_id) && - ((dg.hasOwnProperty("connstate") && - dg.connstate.channel_id != this.channel_id) || - (dg.hasOwnProperty("tunnstate") && - dg.tunnstate.channel_id != this.channel_id)) - ) { - KnxLog.get().trace( - "(%s): *** Ignoring %s datagram for other channel (own: %d)", - this.compositeState(), - descr, - this.channel_id - ); - } else { - // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") - const signal = util.format("inbound_%s", descr); - if (descr === "DISCONNECT_REQUEST") { - KnxLog.get().info("empty internal fsm queue due to %s: ", signal); - this.clearQueue(); - } - this.handle(signal, dg); - } - } catch (err) { - KnxLog.get().debug( - "(%s): Incomplete/unparseable UDP packet: %s: %s", - this.compositeState(), - err, - msg.toString("hex") - ); - } -}; - -KnxFSM.prototype.AddConnState = function (datagram: Datagram): void { - datagram.connstate = { - channel_id: this.channel_id, - state: 0, - }; -}; - -KnxFSM.prototype.AddTunnState = function (datagram: Datagram): void { - // add the remote IP router's endpoint - datagram.tunnstate = { - channel_id: this.channel_id, - tunnel_endpoint: this.remoteEndpoint.addr + ":" + this.remoteEndpoint.port, - }; -}; - -const AddCRI = (datagram: Datagram): void => { - // add the CRI - datagram.cri = { - connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, - knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, - unused: 0, - }; -}; - -KnxFSM.prototype.AddCEMI = function (datagram: Datagram, msgcode: number): void { - const sendAck = - (msgcode || 0x11) == 0x11 && !this.options.suppress_ack_ldatareq; // only for L_Data.req - datagram.cemi = { - msgcode: msgcode || 0x11, // default: L_Data.req for tunneling - ctrl: { - frameType: 1, // 0=extended 1=standard - reserved: 0, // always 0 - repeat: 1, // the OPPOSITE: 1=do NOT repeat - broadcast: 1, // 0-system broadcast 1-broadcast - priority: 3, // 0-system 1-normal 2-urgent 3-low - acknowledge: sendAck ? 1 : 0, - confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error - // 2nd byte - destAddrType: 1, // FIXME: 0-physical 1-groupaddr - hopCount: 6, - extendedFrame: 0, - }, - src_addr: this.options.physAddr || "15.15.15", - dest_addr: "0/0/0", // - apdu: { - // default operation is GroupValue_Write - apci: "GroupValue_Write", - tpci: 0, - data: 0, - }, - }; -}; - -/* - * submit an outbound request to the state machine - * - * type: service type - * datagram_template: - * if a datagram is passed, use this as - * if a function is passed, use this to DECORATE - * if NULL, then just make a new empty datagram. Look at AddXXX methods - */ -KnxFSM.prototype.Request = function ( - type: number, - datagram_template: (datagram: Datagram) => void, - callback: () => void -): void { - // populate skeleton datagram - const datagram = this.prepareDatagram(type); - // decorate the datagram, if a function is passed - if (typeof datagram_template == "function") { - datagram_template(datagram); - } - // make sure that we override the datagram service type! - datagram.service_type = type; - const st = keyText("SERVICE_TYPE", type); - // hand off the outbound request to the state machine - this.handle("outbound_" + st, datagram); - if (typeof callback === "function") callback(); -}; - -// prepare a datagram for the given service type -KnxFSM.prototype.prepareDatagram = function (svcType: number): Datagram { - const datagram: Datagram = { - header_length: 6, - protocol_version: 16, // 0x10 == version 1.0 - service_type: svcType, - total_length: null, // filled in automatically - }; - // - AddHPAI(datagram); - // - switch (svcType) { - case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: - AddTunn(datagram); - AddCRI(datagram); // no break! - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: - case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: - this.AddConnState(datagram); - break; - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - this.AddCEMI(datagram, KnxConstants.MESSAGECODES["L_Data.ind"]); - break; - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - AddTunn(datagram); - this.AddTunnState(datagram); - this.AddCEMI(datagram); - break; - case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - this.AddTunnState(datagram); - break; - default: - KnxLog.get().debug("Do not know how to deal with svc type %d", svcType); - } - return datagram; -}; - -/* -send the datagram over the wire -*/ -KnxFSM.prototype.send = function ( - datagram: Datagram, - callback: (err?: Error) => void -): void { - let cemitype: string; // TODO: set, but unused - try { - this.writer = KnxNetProtocol.createWriter(); - switch (datagram.service_type) { - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - // append the CEMI service type if this is a tunneling request... - cemitype = keyText("MESSAGECODES", datagram.cemi.msgcode); - break; - } - const packet = this.writer.KNXNetHeader(datagram); - const buf = packet.buffer; - const svctype = keyText("SERVICE_TYPE", datagram.service_type); // TODO: unused - const descr = datagramDesc(datagram); - KnxLog.get().trace( - "(%s): Sending %s ==> %j", - this.compositeState(), - descr, - datagram - ); - this.socket.send( - buf, - 0, - buf.length, - this.remoteEndpoint.port, - this.remoteEndpoint.addr.toString(), - (err) => { - KnxLog.get().trace( - "(%s): UDP sent %s: %s %s", - this.compositeState(), - err ? err.toString() : "OK", - descr, - buf.toString("hex") - ); - if (typeof callback === "function") callback(err); - } - ); - } catch (e) { - KnxLog.get().warn(e); - if (typeof callback === "function") callback(e); - } -}; - -KnxFSM.prototype.write = function ( - grpaddr: string, - value: any, - dptid: number, - callback: () => void -): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn("You must supply both grpaddr and value!"); - return; - } - try { - // outbound request onto the state machine - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request( - serviceType, - function (datagram: Datagram) { - populateAPDU(value, datagram.cemi.apdu, dptid); - datagram.cemi.dest_addr = grpaddr; - }, - callback - ); - } catch (e) { - KnxLog.get().warn(e); - } -}; - -KnxFSM.prototype.respond = function ( - grpaddr: string, - value: any, - dptid: number -): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn("You must supply both grpaddr and value!"); - return; - } - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function (datagram: Datagram) { - populateAPDU(value, datagram.cemi.apdu, dptid); - // this is a READ request - datagram.cemi.apdu.apci = "GroupValue_Response"; - datagram.cemi.dest_addr = grpaddr; - return datagram; - }); -}; - -KnxFSM.prototype.writeRaw = function ( - grpaddr: string, - value: Buffer, - bitlength: number, - callback: () => void -): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn("You must supply both grpaddr and value!"); - return; - } - if (!Buffer.isBuffer(value)) { - KnxLog.get().warn("Value must be a buffer!"); - return; - } - // outbound request onto the state machine - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request( - serviceType, - function (datagram: Datagram) { - datagram.cemi.apdu.data = value; - datagram.cemi.apdu.bitlength = bitlength - ? bitlength - : value.byteLength * 8; - datagram.cemi.dest_addr = grpaddr; - }, - callback - ); -}; - -// send a READ request to the bus -// you can pass a callback function which gets bound to the RESPONSE datagram event -KnxFSM.prototype.read = function ( - grpaddr: string, - callback: (src: any, data: any) => void -): void { - if (typeof callback == "function") { - // when the response arrives: - const responseEvent = "GroupValue_Response_" + grpaddr; - KnxLog.get().trace("Binding connection to " + responseEvent); - const binding = (src: any, data: any) => { - // unbind the event handler - this.off(responseEvent, binding); - // fire the callback - callback(src, data); - }; - // prepare for the response - this.on(responseEvent, binding); - // clean up after 3 seconds just in case no one answers the read request - setTimeout(() => this.off(responseEvent, binding), 3000); - } - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function (datagram: Datagram) { - // this is a READ request - datagram.cemi.apdu.apci = "GroupValue_Read"; - datagram.cemi.dest_addr = grpaddr; - return datagram; - }); -}; - -KnxFSM.prototype.Disconnect = function (cb: () => void): void { - var that = this; - - if (this.state === "connecting") { - KnxLog.get().debug("Disconnecting directly"); - that.transition("uninitialized"); - if (cb) { - cb(); - } - return; - } - - KnxLog.get().debug("waiting for Idle-State"); - this.onIdle(function () { - KnxLog.get().trace("In Idle-State"); - - that.on("disconnected", () => { - KnxLog.get().debug("Disconnected from KNX"); - if (cb) { - cb(); - } - }); - - KnxLog.get().debug("Disconnecting from KNX"); - that.transition("disconnecting"); - }); - - // machina.js removeAllListeners equivalent: - // this.off(); -}; - -KnxFSM.prototype.onIdle = function (cb: () => void): void { - if (this.state === "idle") { - KnxLog.get().trace("Connection is already Idle"); - cb(); - } else { - this.on("transition", function (data: any) { - if (data.toState === "idle") { - KnxLog.get().trace("Connection just transitioned to Idle"); - cb(); - } - }); - } -}; - -// return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) -const datagramDesc = (dg: Datagram): string => { - let blurb = keyText("SERVICE_TYPE", dg.service_type); - if ( - dg.service_type == KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || - dg.service_type == KnxConstants.SERVICE_TYPE.ROUTING_INDICATION - ) { - blurb += "_" + keyText("MESSAGECODES", dg.cemi.msgcode); - } - return blurb; -}; - -// add the control udp local endpoint. UPDATE: not needed apparnently? -const AddHPAI = (datagram: Datagram): void => { - datagram.hpai = { - protocol_type: 1, // UDP - //tunnel_endpoint: this.localAddress + ":" + this.control.address().port - tunnel_endpoint: "0.0.0.0:0", - }; -}; - -// add the tunneling udp local endpoint UPDATE: not needed apparently? -const AddTunn = (datagram: Datagram): void => { - datagram.tunn = { - protocol_type: 1, // UDP - tunnel_endpoint: "0.0.0.0:0", - //tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port - }; -}; - -// TODO: Conncetion is obviously not a constructor, but tests call it with `new`. That should be deprecated. -function Connection(options: any): any { - const conn = new KnxFSM(options); - // register with the FSM any event handlers passed into the options object - if (typeof options.handlers === "object") { - for (const [key, value] of Object.entries(options.handlers)) { - if (typeof value === "function") { - conn.on(key, value); - } - } - } - // boot up the KNX connection unless told otherwise - if (!options.manualConnect) conn.Connect(); - return conn; -} - -export default Connection; diff --git a/src/FSM.ts b/src/FSM.ts index 0ae791a..c08438c 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -6,7 +6,10 @@ import { keyText, KnxConstants } from "./KnxConstants.js"; import IpRoutingConnection from "./IpRoutingConnection.js"; import IpTunnelingConnection from "./IpTunnelingConnection.js"; import KnxLog, { KnxLogOptions } from "./KnxLog.js"; -import EventEmitter from "events"; +import KnxNetProtocol from "./KnxProtocol"; +import { Writer } from "binary-protocol"; +import { Socket } from "dgram"; +import { populateAPDU } from "./dptlib/index.js"; type KnxDeviceAddress = string; @@ -81,7 +84,7 @@ export interface Datagram { destAddrType: number; hopCount: number; extendedFrame: number; - } + }; }; tunnstate?: { seqnum?: number; @@ -92,22 +95,22 @@ export interface Datagram { tunn?: { protocol_type: number; tunnel_endpoint: string; - } + }; hpai?: { header_length?: number; protocol_type: number; tunnel_endpoint: string; - } + }; connstate?: { state: number; channel_id: number; status?: number; - }, + }; cri?: { connection_type: number; knx_layer: number; unused: number; - } + }; } export class KnxFSM extends machina.FSM { @@ -134,8 +137,27 @@ export class KnxFSM extends machina.FSM { private seqnum: number; private seqnumRecv: number; + private writer: Writer; + private socket: Socket; + public localAddress: string | null; + constructor(options: KnxOptions) { + super(); + + this.options = options || {}; + + if (typeof options.handlers === "object") { + for (const [key, value] of Object.entries(options.handlers)) { + if (typeof value === "function") { + this.on(key, value); + } + } + } + // boot up the KNX connection unless told otherwise + if (!options.manualConnect) this.Connect(); + } + initialize(options: KnxOptions) { this.options = options || {}; this.log = KnxLog.get(options); @@ -578,10 +600,7 @@ export class KnxFSM extends machina.FSM { }; acknowledge(datagram: Datagram) { - const ack = this.prepareDatagram( - KnxConstants.SERVICE_TYPE.TUNNELING_ACK, - datagram - ); + const ack = this.prepareDatagram(KnxConstants.SERVICE_TYPE.TUNNELING_ACK); ack.tunnstate.seqnum = datagram.tunnstate.seqnum; this.send(ack, (err: any) => {}); } @@ -649,18 +668,412 @@ export class KnxFSM extends machina.FSM { return candidateInterfaces; } - BindSocket(cb: (socket: any) => void) {} + BindSocket(cb: (socket: any) => void) { + // THIS IS A STUB and should be overridden by the connection type + } + + Connect() { + // THIS IS A STUB and should be overridden by the connection type + } + + /** + * -------------------------------- + * CONNECTION MANAGEMENT + * -------------------------------- + */ + + /** Bind incoming UDP packet handler */ + onUdpSocketMessage(msg: Buffer, rinfo: any, callback: () => void): void { + // get the incoming packet's service type ... + try { + const reader = KnxNetProtocol.createReader(msg); + // TODO: improve types for binary protocol + (reader as any).KNXNetHeader("tmp"); + const dg = reader.next()["tmp"]; + const descr = datagramDesc(dg); + KnxLog.get().trace( + "(%s): Received %s message: %j", + this.compositeState(), + descr, + dg + ); + if ( + !isNaN(this.channel_id) && + ((dg.hasOwnProperty("connstate") && + dg.connstate.channel_id != this.channel_id) || + (dg.hasOwnProperty("tunnstate") && + dg.tunnstate.channel_id != this.channel_id)) + ) { + KnxLog.get().trace( + "(%s): *** Ignoring %s datagram for other channel (own: %d)", + this.compositeState(), + descr, + this.channel_id + ); + } else { + // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") + const signal = util.format("inbound_%s", descr); + if (descr === "DISCONNECT_REQUEST") { + KnxLog.get().info("empty internal fsm queue due to %s: ", signal); + this.clearQueue(); + } + this.handle(signal, dg); + } + } catch (err) { + KnxLog.get().debug( + "(%s): Incomplete/unparseable UDP packet: %s: %s", + this.compositeState(), + err, + msg.toString("hex") + ); + } + } + + AddConnState(datagram: Datagram): void { + datagram.connstate = { + channel_id: this.channel_id, + state: 0, + }; + } - Connect() {} + AddTunnState(datagram: Datagram): void { + // add the remote IP router's endpoint + datagram.tunnstate = { + channel_id: this.channel_id, + tunnel_endpoint: + this.remoteEndpoint.addr + ":" + this.remoteEndpoint.port, + }; + } + + AddCRI = (datagram: Datagram): void => { + // add the CRI + datagram.cri = { + connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, + knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, + unused: 0, + }; + }; - prepareDatagram( - serviceType: number, - datagram?: Datagram - ): Datagram { - return undefined as Datagram + AddCEMI(datagram: Datagram, msgcode?: number): void { + const sendAck = + (msgcode || 0x11) == 0x11 && !this.options.suppress_ack_ldatareq; // only for L_Data.req + datagram.cemi = { + msgcode: msgcode || 0x11, // default: L_Data.req for tunneling + ctrl: { + frameType: 1, // 0=extended 1=standard + reserved: 0, // always 0 + repeat: 1, // the OPPOSITE: 1=do NOT repeat + broadcast: 1, // 0-system broadcast 1-broadcast + priority: 3, // 0-system 1-normal 2-urgent 3-low + acknowledge: sendAck ? 1 : 0, + confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error + // 2nd byte + destAddrType: 1, // FIXME: 0-physical 1-groupaddr + hopCount: 6, + extendedFrame: 0, + }, + src_addr: this.options.physAddr || "15.15.15", + dest_addr: "0/0/0", // + apdu: { + // default operation is GroupValue_Write + apci: "GroupValue_Write", + tpci: 0, + data: 0, + }, + }; + } + + /* + * submit an outbound request to the state machine + * + * type: service type + * datagram_template: + * if a datagram is passed, use this as + * if a function is passed, use this to DECORATE + * if NULL, then just make a new empty datagram. Look at AddXXX methods + */ + Request( + type: number, + datagram_template: (datagram: Datagram) => void, + callback?: () => void + ): void { + // populate skeleton datagram + const datagram = this.prepareDatagram(type); + // decorate the datagram, if a function is passed + if (typeof datagram_template == "function") { + datagram_template(datagram); + } + // make sure that we override the datagram service type! + datagram.service_type = type; + const st = keyText("SERVICE_TYPE", type); + // hand off the outbound request to the state machine + this.handle("outbound_" + st, datagram); + if (typeof callback === "function") callback(); } - send(datagram: Datagram, cb?: (err: Error) => void) {} + // prepare a datagram for the given service type + prepareDatagram(svcType: number): Datagram { + const datagram: Datagram = { + header_length: 6, + protocol_version: 16, // 0x10 == version 1.0 + service_type: svcType, + total_length: null, // filled in automatically + }; + // + AddHPAI(datagram); + // + switch (svcType) { + case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: + AddTunn(datagram); + this.AddCRI(datagram); // no break! + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: + case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: + this.AddConnState(datagram); + break; + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + this.AddCEMI(datagram, KnxConstants.MESSAGECODES["L_Data.ind"]); + break; + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + AddTunn(datagram); + this.AddTunnState(datagram); + this.AddCEMI(datagram); + break; + case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: + this.AddTunnState(datagram); + break; + default: + KnxLog.get().debug("Do not know how to deal with svc type %d", svcType); + } + return datagram; + } + + /* + send the datagram over the wire + */ + send(datagram: Datagram, callback: (err?: Error) => void): void { + let cemitype: string; // TODO: set, but unused + try { + this.writer = KnxNetProtocol.createWriter(); + switch (datagram.service_type) { + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + // append the CEMI service type if this is a tunneling request... + cemitype = keyText("MESSAGECODES", datagram.cemi.msgcode); + break; + } + const packet = (this.writer as any).KNXNetHeader(datagram); + const buf = packet.buffer; + const svctype = keyText("SERVICE_TYPE", datagram.service_type); // TODO: unused + const descr = datagramDesc(datagram); + KnxLog.get().trace( + "(%s): Sending %s ==> %j", + this.compositeState(), + descr, + datagram + ); + this.socket.send( + buf, + 0, + buf.length, + this.remoteEndpoint.port, + this.remoteEndpoint.addr.toString(), + (err) => { + KnxLog.get().trace( + "(%s): UDP sent %s: %s %s", + this.compositeState(), + err ? err.toString() : "OK", + descr, + buf.toString("hex") + ); + if (typeof callback === "function") callback(err); + } + ); + } catch (e) { + KnxLog.get().warn(e); + if (typeof callback === "function") callback(e); + } + } + + write( + grpaddr: string, + value: any, + dptid: number, + callback: () => void + ): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn("You must supply both grpaddr and value!"); + return; + } + try { + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request( + serviceType, + function (datagram: Datagram) { + populateAPDU(value, datagram.cemi.apdu, dptid); + datagram.cemi.dest_addr = grpaddr; + }, + callback + ); + } catch (e) { + KnxLog.get().warn(e); + } + } + + respond(grpaddr: string, value: any, dptid: number): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn("You must supply both grpaddr and value!"); + return; + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request(serviceType, function (datagram: Datagram) { + populateAPDU(value, datagram.cemi.apdu, dptid); + // this is a READ request + datagram.cemi.apdu.apci = "GroupValue_Response"; + datagram.cemi.dest_addr = grpaddr; + return datagram; + }); + } + + writeRaw( + grpaddr: string, + value: Buffer, + bitlength: number, + callback: () => void + ): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn("You must supply both grpaddr and value!"); + return; + } + if (!Buffer.isBuffer(value)) { + KnxLog.get().warn("Value must be a buffer!"); + return; + } + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request( + serviceType, + function (datagram: Datagram) { + datagram.cemi.apdu.data = value; + datagram.cemi.apdu.bitlength = bitlength + ? bitlength + : value.byteLength * 8; + datagram.cemi.dest_addr = grpaddr; + }, + callback + ); + } + + // send a READ request to the bus + // you can pass a callback function which gets bound to the RESPONSE datagram event + read(grpaddr: string, callback: (src: any, data: any) => void): void { + if (typeof callback == "function") { + // when the response arrives: + const responseEvent = "GroupValue_Response_" + grpaddr; + KnxLog.get().trace("Binding connection to " + responseEvent); + const binding = (src: any, data: any) => { + // unbind the event handler + this.off(responseEvent, binding); + // fire the callback + callback(src, data); + }; + // prepare for the response + this.on(responseEvent, binding); + // clean up after 3 seconds just in case no one answers the read request + setTimeout(() => this.off(responseEvent, binding), 3000); + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; + this.Request(serviceType, function (datagram: Datagram) { + // this is a READ request + datagram.cemi.apdu.apci = "GroupValue_Read"; + datagram.cemi.dest_addr = grpaddr; + return datagram; + }); + } + + Disconnect(cb: () => void): void { + + if (this.state === "connecting") { + KnxLog.get().debug("Disconnecting directly"); + this.transition("uninitialized"); + if (cb) { + cb(); + } + return; + } + + KnxLog.get().debug("waiting for Idle-State"); + this.onIdle(() => { + KnxLog.get().trace("In Idle-State"); + + this.on("disconnected", () => { + KnxLog.get().debug("Disconnected from KNX"); + if (cb) { + cb(); + } + }); + + KnxLog.get().debug("Disconnecting from KNX"); + this.transition("disconnecting"); + }); + + // machina.js removeAllListeners equivalent: + // this.off(); + } + + onIdle(cb: () => void): void { + if (this.state === "idle") { + KnxLog.get().trace("Connection is already Idle"); + cb(); + } else { + this.on("transition", function (data: any) { + if (data.toState === "idle") { + KnxLog.get().trace("Connection just transitioned to Idle"); + cb(); + } + }); + } + } } -export default machina.FSM.extend(KnxFSM); +// return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) +const datagramDesc = (dg: Datagram): string => { + let blurb = keyText("SERVICE_TYPE", dg.service_type); + if ( + dg.service_type == KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || + dg.service_type == KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + ) { + blurb += "_" + keyText("MESSAGECODES", dg.cemi.msgcode); + } + return blurb; +}; + +// add the control udp local endpoint. UPDATE: not needed apparnently? +const AddHPAI = (datagram: Datagram): void => { + datagram.hpai = { + protocol_type: 1, // UDP + //tunnel_endpoint: this.localAddress + ":" + this.control.address().port + tunnel_endpoint: "0.0.0.0:0", + }; +}; + +// add the tunneling udp local endpoint UPDATE: not needed apparently? +const AddTunn = (datagram: Datagram): void => { + datagram.tunn = { + protocol_type: 1, // UDP + tunnel_endpoint: "0.0.0.0:0", + //tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port + }; +}; + +const KnxFSMConnection = machina.FSM.extend(KnxFSM) + +export default KnxFSMConnection; diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index afcaad7..716ca5a 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -1,9 +1,9 @@ import * as util from 'util'; import * as dgram from 'dgram'; import KnxLog from './KnxLog.js'; -import KnxNet from './FSM.js'; +import { KnxFSM } from './FSM.js'; -function IpRoutingConnection(instance: KnxNet): KnxNet { +function IpRoutingConnection(instance: KnxFSM): KnxFSM { const log = KnxLog.get(); instance.BindSocket = function (cb: (socket: dgram.Socket) => void): dgram.Socket { @@ -34,7 +34,7 @@ function IpRoutingConnection(instance: KnxNet): KnxNet { // /// Start the connection /// - instance.Connect = function (): KnxNet { + instance.Connect = function (): KnxFSM { this.localAddress = this.getLocalAddress(); this.socket = this.BindSocket((socket: dgram.Socket) => { socket.on('error', (errmsg: string) => diff --git a/src/IpTunnelingConnection.ts b/src/IpTunnelingConnection.ts index b58f807..c957425 100644 --- a/src/IpTunnelingConnection.ts +++ b/src/IpTunnelingConnection.ts @@ -1,16 +1,17 @@ -import dgram from 'dgram'; -import KnxLog from './KnxLog'; -import KnxNet from './FSM'; +import dgram from "dgram"; +import KnxLog from "./KnxLog"; +import { KnxFSM } from "./FSM"; - -function IpTunnelingConnection(instance: KnxNet): KnxNet { +function IpTunnelingConnection(instance: KnxFSM): KnxFSM { const log = KnxLog.get(); - instance.BindSocket = function (cb: (socket: dgram.Socket) => void): dgram.Socket { - const udpSocket = dgram.createSocket('udp4'); + instance.BindSocket = function ( + cb: (socket: dgram.Socket) => void + ): dgram.Socket { + const udpSocket = dgram.createSocket("udp4"); udpSocket.bind(() => { log.debug( - 'IpTunnelingConnection.BindSocket %s:%d', + "IpTunnelingConnection.BindSocket %s:%d", instance.localAddress, udpSocket.address().port ); @@ -19,17 +20,22 @@ function IpTunnelingConnection(instance: KnxNet): KnxNet { return udpSocket; }; - instance.Connect = function (): IpTunnelingConnectionInstance { + instance.Connect = function (): KnxFSM { this.localAddress = this.getLocalAddress(); // create the socket this.socket = this.BindSocket((socket: dgram.Socket) => { - socket.on('error', (errmsg: string) => log.debug('Socket error: %j', errmsg)); - socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo, callback: () => void) => { - log.debug('Inbound message: %s', msg.toString('hex')); - this.onUdpSocketMessage(msg, rinfo, callback); - }); + socket.on("error", (errmsg: string) => + log.debug("Socket error: %j", errmsg) + ); + socket.on( + "message", + (msg: Buffer, rinfo: dgram.RemoteInfo, callback: () => void) => { + log.debug("Inbound message: %s", msg.toString("hex")); + this.onUdpSocketMessage(msg, rinfo, callback); + } + ); // start connection sequence - this.transition('connecting'); + this.transition("connecting"); }); return this; }; @@ -37,4 +43,4 @@ function IpTunnelingConnection(instance: KnxNet): KnxNet { return instance; } -export default IpTunnelingConnection; \ No newline at end of file +export default IpTunnelingConnection; diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index db33e52..0b05819 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -554,6 +554,7 @@ KnxProtocol.define("CEMI", { } }, }); + KnxProtocol.lengths["CEMI"] = (value: Datagram["cemi"]) => { if (!value) return 0; const apdu_length = knxlen("APDU", value.apdu); diff --git a/src/index.ts b/src/index.ts index 30c5cf8..d681427 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { format } from 'util'; import log from 'log-driver'; -import Connection from './Connection'; +import Connection from './FSM'; import Datapoint from './Datapoint'; import Devices from './devices'; import Log from './KnxLog'; diff --git a/src/types/binary-protocol.d.ts b/src/types/binary-protocol.d.ts index 637bd84..132f66d 100644 --- a/src/types/binary-protocol.d.ts +++ b/src/types/binary-protocol.d.ts @@ -1,10 +1,51 @@ +import { Duplex } from "stream"; + declare module "binary-protocol" { + interface ProtocolConfig { + read(propertyName: string): void; + write(value: any): void; + } + + interface Reader { + define(name: string, config: ProtocolConfig): this; + clone(): this; + demand(howMany: number): this; + tap(callback: (value: any) => void): this; + enqueue(name: string, arg1: any, arg2: any): this; + prepend(fn: (arg: any) => void, arg1: any): this; + pushStack(item: any): this; + popStack(property: string, fn: (value: any) => void): this; + collect(property: string, fn: (value: any) => void): this; + loop(property: string, fn: (value: any) => void): this; + end(fn: () => void): this; + finally(fn: () => void): this; + reset(): this; + raw(property: string, fn: (value: any) => void): this; + next(chunk?: any): any; + process(): any; + createLooper(property: string, fn: (value: any) => void): this; + } + + interface Writer { + define(name: string, config: ProtocolConfig): this; + clone(): this; + allocate(howMany: number): this; + raw(buffer: Buffer): this; + forward(howMany: number): this; + tap(callback: (value: any) => void): this; + } + + interface Commander { + define(name: string, config: ProtocolConfig): this; + clone(): this; + createReadStream(options: any): Reader; + createWriteStream(options: any): Writer; + } -interface ProtocolConfig { - read(propertyName: string): void - write(value: any): void -} export class BinaryProtocol { - define(name: string, config: ProtocolConfig): this + define(name: string, config: ProtocolConfig): this; + createReader(buffer: Buffer, offset?: number): Reader; + createWriter(buffer?: Buffer, offset?: number): Writer; + createCommander(duplex: Duplex): Commander; } } diff --git a/src/types/machina.d.ts b/src/types/machina.d.ts index b451892..2947fbd 100644 --- a/src/types/machina.d.ts +++ b/src/types/machina.d.ts @@ -28,13 +28,23 @@ declare module "machina" { export class BehavioralFsm extends EventEmitter { initialState: string; namespace: string; - states: any// Record; - - constructor(options: Options); - static extend(protoProps: T, staticProps?: any): this; + states: any; // Record; + state: string; + + static extend( + protoProps: T, + staticProps?: any + ): T & BehavioralFsm; + compositeState(client: BehavioralFsm): any; + clearQueue(client: BehavioralFsm, name?: string): void; + handle(client: BehavioralFsm, ...args: any[]): any; + transition(client: BehavioralFsm, newState: string): void; } export class FSM extends BehavioralFsm { - constructor(options: Options); + compositeState(): any; + clearQueue(name?: string): void; + handle(...args: any[]): any; + transition(newState: string): void; } } From e8ef57db7d567dca13b85e835455d8d1375ff654 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 10:34:09 +0200 Subject: [PATCH 03/36] feat: convert tests to ts --- .gitignore | 1 + package-lock.json | 20 ++++ package.json | 5 +- src/Address.ts | 9 +- src/Datapoint.ts | 1 + src/FSM.ts | 12 +-- src/KnxProtocol.ts | 2 +- src/index.ts | 4 +- src/types/binary-protocol.d.ts | 70 +++++++------- ...ect-routing.js => test-connect-routing.ts} | 10 +- test/dptlib/commontest.js | 63 ------------ test/dptlib/commontest.ts | 73 ++++++++++++++ test/dptlib/{test-dpt.js => test-dpt.ts} | 27 +++--- test/dptlib/test-dpt1.js | 8 -- test/dptlib/test-dpt1.ts | 10 ++ test/dptlib/{test-dpt10.js => test-dpt10.ts} | 16 ++-- test/dptlib/{test-dpt11.js => test-dpt11.ts} | 20 ++-- test/dptlib/{test-dpt12.js => test-dpt12.ts} | 4 +- test/dptlib/{test-dpt13.js => test-dpt13.ts} | 4 +- test/dptlib/{test-dpt19.js => test-dpt19.ts} | 13 +-- test/dptlib/{test-dpt2.js => test-dpt2.ts} | 4 +- test/dptlib/{test-dpt21.js => test-dpt21.ts} | 6 +- .../dptlib/{test-dpt237.js => test-dpt237.ts} | 6 +- test/dptlib/{test-dpt3.js => test-dpt3.ts} | 7 +- test/dptlib/{test-dpt4.js => test-dpt4.ts} | 5 +- test/dptlib/{test-dpt5.js => test-dpt5.ts} | 10 +- test/dptlib/{test-dpt6.js => test-dpt6.ts} | 4 +- test/dptlib/{test-dpt7.js => test-dpt7.ts} | 4 +- test/dptlib/{test-dpt8.js => test-dpt8.ts} | 4 +- test/dptlib/{test-dpt9.js => test-dpt9.ts} | 4 +- .../{test-address.js => test-address.ts} | 35 ++++--- .../knxproto/{test-proto.js => test-proto.ts} | 22 ++--- ...brid.js => test-connect-routing-hybrid.ts} | 12 +-- ...nnect-tunnel.js => test-connect-tunnel.ts} | 10 +- .../{test-control.js => test-control.ts} | 12 +-- .../{test-logotimer.js => test-logotimer.ts} | 15 ++- .../wiredtests/{test-read.js => test-read.ts} | 12 +-- test/wiredtests/test-readstorm.js | 77 --------------- test/wiredtests/test-readstorm.ts | 95 +++++++++++++++++++ ...edtest-options.js => wiredtest-options.ts} | 4 +- 40 files changed, 394 insertions(+), 326 deletions(-) rename test/connection/{test-connect-routing.js => test-connect-routing.ts} (76%) delete mode 100644 test/dptlib/commontest.js create mode 100644 test/dptlib/commontest.ts rename test/dptlib/{test-dpt.js => test-dpt.ts} (69%) delete mode 100644 test/dptlib/test-dpt1.js create mode 100644 test/dptlib/test-dpt1.ts rename test/dptlib/{test-dpt10.js => test-dpt10.ts} (82%) rename test/dptlib/{test-dpt11.js => test-dpt11.ts} (70%) rename test/dptlib/{test-dpt12.js => test-dpt12.ts} (90%) rename test/dptlib/{test-dpt13.js => test-dpt13.ts} (91%) rename test/dptlib/{test-dpt19.js => test-dpt19.ts} (79%) rename test/dptlib/{test-dpt2.js => test-dpt2.ts} (86%) rename test/dptlib/{test-dpt21.js => test-dpt21.ts} (91%) rename test/dptlib/{test-dpt237.js => test-dpt237.ts} (93%) rename test/dptlib/{test-dpt3.js => test-dpt3.ts} (82%) rename test/dptlib/{test-dpt4.js => test-dpt4.ts} (76%) rename test/dptlib/{test-dpt5.js => test-dpt5.ts} (82%) rename test/dptlib/{test-dpt6.js => test-dpt6.ts} (82%) rename test/dptlib/{test-dpt7.js => test-dpt7.ts} (84%) rename test/dptlib/{test-dpt8.js => test-dpt8.ts} (85%) rename test/dptlib/{test-dpt9.js => test-dpt9.ts} (90%) rename test/knxproto/{test-address.js => test-address.ts} (71%) rename test/knxproto/{test-proto.js => test-proto.ts} (93%) rename test/wiredtests/{test-connect-routing-hybrid.js => test-connect-routing-hybrid.ts} (79%) rename test/wiredtests/{test-connect-tunnel.js => test-connect-tunnel.ts} (85%) rename test/wiredtests/{test-control.js => test-control.ts} (89%) rename test/wiredtests/{test-logotimer.js => test-logotimer.ts} (70%) rename test/wiredtests/{test-read.js => test-read.ts} (85%) delete mode 100644 test/wiredtests/test-readstorm.js create mode 100644 test/wiredtests/test-readstorm.ts rename test/wiredtests/{wiredtest-options.js => wiredtest-options.ts} (95%) diff --git a/.gitignore b/.gitignore index 876c9a9..7b3c43e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .tags* node_modules +build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c05848a..84a526e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@types/node": "^20.12.2", + "@types/tape": "^5.6.4", "esbuild-register": "^3.5.0", "multi-tape": "^1.2.1", "tape": "^4.10.1", @@ -426,6 +427,25 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/tape": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/@types/tape/-/tape-5.6.4.tgz", + "integrity": "sha512-EmL4fJpZyByNCkupLLcJhneqcnT+rQUG5fWKNCsZyBK1x7nUuDTwwEerc4biEMZgvSK2+FXr775aLeXhKXK4Yw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/through": "*" + } + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", diff --git a/package.json b/package.json index ea87eef..0040264 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "url": "https://github.com/ekarak/knx.git" }, "scripts": { - "test": "multi-tape test/*/test-*.js" + "build": "tsc -p tsconfig.build.json", + "test": "multi-tape --node-arg='-r esbuild-register' test/*/test-*.ts" }, + "main": "./build/index.js", "license": "MIT", "author": { "name": "Elias Karakoulakis", @@ -96,6 +98,7 @@ }, "devDependencies": { "@types/node": "^20.12.2", + "@types/tape": "^5.6.4", "esbuild-register": "^3.5.0", "multi-tape": "^1.2.1", "tape": "^4.10.1", diff --git a/src/Address.ts b/src/Address.ts index 921be8b..3c23787 100644 --- a/src/Address.ts +++ b/src/Address.ts @@ -41,7 +41,7 @@ interface AddressType { // +--+--------------------+-----------------------+ // NOTE: ets4 can utilise all 5 bits for the main group (0..31) -const TYPE: AddressType = { +export const TYPE: AddressType = { PHYSICAL: 0x00, GROUP: 0x01, }; @@ -51,7 +51,7 @@ const threeLevelGroup = new Parser().bit5('l1').bit3('l2').uint8('l3'); const twoLevel = new Parser().bit5('l1').bit11('l2'); // convert address stored in two-byte buffer to string -const toString = function ( +export function toString ( buf: string | Buffer, addrtype: number, twoLevelAddressing = false @@ -75,9 +75,9 @@ const toString = function ( // check for out of range integer const r = (x: number, max: number): boolean => x < 0 || x > max; // parse address string to 2-byte Buffer -const parse = function ( +export function parse( addr: string, - addrtype: number, + addrtype?: number, twoLevelAddressing = false ): Buffer { if (!addr) { @@ -117,4 +117,3 @@ const parse = function ( return address; }; -export default { TYPE, toString, parse }; \ No newline at end of file diff --git a/src/Datapoint.ts b/src/Datapoint.ts index 8afb417..fc23803 100644 --- a/src/Datapoint.ts +++ b/src/Datapoint.ts @@ -7,6 +7,7 @@ interface Options { ga: string; dpt?: string; autoread?: boolean; + status_ga?: string; } class Datapoint extends EventEmitter { diff --git a/src/FSM.ts b/src/FSM.ts index c08438c..aaefb6d 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -688,7 +688,7 @@ export class KnxFSM extends machina.FSM { try { const reader = KnxNetProtocol.createReader(msg); // TODO: improve types for binary protocol - (reader as any).KNXNetHeader("tmp"); + reader.KNXNetHeader("tmp"); const dg = reader.next()["tmp"]; const descr = datagramDesc(dg); KnxLog.get().trace( @@ -861,7 +861,7 @@ export class KnxFSM extends machina.FSM { cemitype = keyText("MESSAGECODES", datagram.cemi.msgcode); break; } - const packet = (this.writer as any).KNXNetHeader(datagram); + const packet = this.writer.KNXNetHeader(datagram); const buf = packet.buffer; const svctype = keyText("SERVICE_TYPE", datagram.service_type); // TODO: unused const descr = datagramDesc(datagram); @@ -897,8 +897,8 @@ export class KnxFSM extends machina.FSM { write( grpaddr: string, value: any, - dptid: number, - callback: () => void + dptid?: number, + callback?: () => void ): void { if (grpaddr == null || value == null) { KnxLog.get().warn("You must supply both grpaddr and value!"); @@ -942,8 +942,8 @@ export class KnxFSM extends machina.FSM { writeRaw( grpaddr: string, value: Buffer, - bitlength: number, - callback: () => void + bitlength?: number, + callback?: () => void ): void { if (grpaddr == null || value == null) { KnxLog.get().warn("You must supply both grpaddr and value!"); diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index 0b05819..80643b8 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -7,7 +7,7 @@ import util from "util"; import ipaddr from "ipaddr.js"; import { Parser } from 'binary-parser' import { BinaryProtocol } from "binary-protocol"; -import KnxAddress from "./Address"; +import * as KnxAddress from "./Address"; import { APCICODES, keyText, KnxConstants } from "./KnxConstants"; import KnxLog from "./KnxLog"; import { Datagram } from "./FSM"; diff --git a/src/index.ts b/src/index.ts index d681427..8983350 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,11 +9,11 @@ import log from 'log-driver'; import Connection from './FSM'; import Datapoint from './Datapoint'; import Devices from './devices'; -import Log from './KnxLog'; +import Log, { LogLevel } from './KnxLog'; const pkgJson = require('../package.json'); log.info(format('Loading %s: %s, version: %s', pkgJson.name, pkgJson.description, pkgJson.version)); -export { Connection, Datapoint, Devices, Log }; \ No newline at end of file +export { Connection, Datapoint, Devices, Log, LogLevel }; \ No newline at end of file diff --git a/src/types/binary-protocol.d.ts b/src/types/binary-protocol.d.ts index 132f66d..c1bbee8 100644 --- a/src/types/binary-protocol.d.ts +++ b/src/types/binary-protocol.d.ts @@ -6,46 +6,50 @@ declare module "binary-protocol" { write(value: any): void; } - interface Reader { - define(name: string, config: ProtocolConfig): this; - clone(): this; - demand(howMany: number): this; - tap(callback: (value: any) => void): this; - enqueue(name: string, arg1: any, arg2: any): this; - prepend(fn: (arg: any) => void, arg1: any): this; - pushStack(item: any): this; - popStack(property: string, fn: (value: any) => void): this; - collect(property: string, fn: (value: any) => void): this; - loop(property: string, fn: (value: any) => void): this; - end(fn: () => void): this; - finally(fn: () => void): this; - reset(): this; - raw(property: string, fn: (value: any) => void): this; - next(chunk?: any): any; - process(): any; - createLooper(property: string, fn: (value: any) => void): this; - } + interface Reader { + define(name: string, config: ProtocolConfig): this; + clone(): this; + demand(howMany: number): this; + tap(callback: (value: any) => void): this; + enqueue(name: string, arg1: any, arg2: any): this; + prepend(fn: (arg: any) => void, arg1: any): this; + pushStack(item: any): this; + popStack(property: string, fn: (value: any) => void): this; + collect(property: string, fn: (value: any) => void): this; + loop(property: string, fn: (value: any) => void): this; + end(fn: () => void): this; + finally(fn: () => void): this; + reset(): this; + raw(property: string, fn: (value: any) => void): this; + next(chunk?: any): any; + process(): any; + createLooper(property: string, fn: (value: any) => void): this; + [key: string]: any; // method created with `define` + } - interface Writer { - define(name: string, config: ProtocolConfig): this; - clone(): this; - allocate(howMany: number): this; - raw(buffer: Buffer): this; - forward(howMany: number): this; - tap(callback: (value: any) => void): this; - } + interface Writer { + buffer: Buffer; + define(name: string, config: ProtocolConfig): this; + clone(): this; + allocate(howMany: number): this; + raw(buffer: Buffer): this; + forward(howMany: number): this; + tap(callback: (value: any) => void): this; + [key: string]: any; // method created with `define` + } - interface Commander { - define(name: string, config: ProtocolConfig): this; - clone(): this; - createReadStream(options: any): Reader; - createWriteStream(options: any): Writer; - } + interface Commander { + define(name: string, config: ProtocolConfig): this; + clone(): this; + createReadStream(options: any): Reader; + createWriteStream(options: any): Writer; + } export class BinaryProtocol { define(name: string, config: ProtocolConfig): this; createReader(buffer: Buffer, offset?: number): Reader; createWriter(buffer?: Buffer, offset?: number): Writer; createCommander(duplex: Duplex): Commander; + [key: string]: any; // method created with `define` } } diff --git a/test/connection/test-connect-routing.js b/test/connection/test-connect-routing.ts similarity index 76% rename from test/connection/test-connect-routing.js rename to test/connection/test-connect-routing.ts index fbc7182..2e4d54f 100644 --- a/test/connection/test-connect-routing.js +++ b/test/connection/test-connect-routing.ts @@ -5,15 +5,13 @@ Error.stackTraceLimit = Infinity; -const knx = require('../..'); -const address = require('../../src/Address.js'); -const assert = require('assert'); -const test = require('tape'); +import { Connection, LogLevel } from '../../src'; +import test from 'tape'; // test('KNX connect routing', function(t) { - var connection = knx.Connection({ - loglevel: 'trace', + var connection = new Connection({ + loglevel: LogLevel.Trace, handlers: { connected: function() { console.log('----------'); diff --git a/test/dptlib/commontest.js b/test/dptlib/commontest.js deleted file mode 100644 index c900a4e..0000000 --- a/test/dptlib/commontest.js +++ /dev/null @@ -1,63 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -const test = require('tape'); -const DPTLib = require('../../src/dptlib'); -const assert = require('assert'); - -/* common DPT unit test. Tries to -- 1. marshal a JS value into an apdu.data (Buffer) and compare it output to the expected value -- 2. unmarshal the produced APDU from step 1 and compare it to the initial JS value -- 3. unmarshal the expected APDU from the test definition and compare it to the initial JS value -*/ - -// marshalling test (JS value to APDU) -function marshalTest(t, dptid, jsval, apdu) { - var marshalled = {}; - DPTLib.populateAPDU(jsval, marshalled, dptid); - // console.log('%j --> %j', apdu.constructor.name, marshalled.data) - t.deepEqual(marshalled.data, apdu, - `${dptid}.populateAPDU(${jsval}:${typeof jsval}) should be marshalled as \"0x${apdu.toString('hex')}\", got: \"0x${marshalled.data.toString('hex')}\"` - ); - return marshalled.data; -}; - -function unmarshalTest(t, dptid, jsval, data) { - var dpt = DPTLib.resolve(dptid); - var unmarshalled = DPTLib.fromBuffer(data, dpt); - //console.log('%s: %j --> %j', dpt.id, rhs, converted); - var msg = `${dptid}.fromBuffer(${JSON.stringify(data)}) should unmarshall to ${JSON.stringify(jsval)}, got: ${JSON.stringify(unmarshalled)}` - switch (typeof jsval) { - case 'object': - t.deepEqual(unmarshalled, jsval, msg); - break; - case 'number': - t.equal(unmarshalled, jsval, msg); - break; - default: - t.ok(unmarshalled == jsval, msg); - } -}; - -module.exports = { - do: function(dptid, tests) { - var dpt = DPTLib.resolve(dptid); - var desc = (dpt.hasOwnProperty('subtype') && dpt.subtype.desc) || dpt.basetype.desc; - test(`${dptid}: ${desc}`, function(t) { - Object.keys(tests).forEach(function(key) { - var apdu = new Buffer(tests[key].apdu_data); - var jsval = tests[key].jsval; - //console.log(dptid + ': apdu=%j jsval=%j', apdu, jsval); - // 1. marshalling test (JS value to APDU) - var marshalled_data = marshalTest(t, dptid, jsval, apdu); - // 2. unmarshal from APDU produced by step 1 - unmarshalTest(t, dptid, jsval, marshalled_data); - // 3. unmarshal from test APDU - unmarshalTest(t, dptid, jsval, apdu); - }); - t.end(); - }); - } -} diff --git a/test/dptlib/commontest.ts b/test/dptlib/commontest.ts new file mode 100644 index 0000000..a58b21a --- /dev/null +++ b/test/dptlib/commontest.ts @@ -0,0 +1,73 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import test, { Test } from "tape"; +import DPTLib, { fromBuffer, populateAPDU, resolve } from "../../src/dptlib"; +import { Datagram } from "../../src/FSM"; + +/* common DPT unit test. Tries to +- 1. marshal a JS value into an apdu.data (Buffer) and compare it output to the expected value +- 2. unmarshal the produced APDU from step 1 and compare it to the initial JS value +- 3. unmarshal the expected APDU from the test definition and compare it to the initial JS value +*/ + +// marshalling test (JS value to APDU) +function marshalTest(t: Test, dptid: string | number, jsval: any, apdu: any) { + const marshalled = {} as Datagram["cemi"]["apdu"]; + populateAPDU(jsval, marshalled, dptid); + // console.log('%j --> %j', apdu.constructor.name, marshalled.data) + t.deepEqual( + marshalled.data, + apdu, + `${dptid}.populateAPDU(${jsval}:${typeof jsval}) should be marshalled as \"0x${apdu.toString( + "hex" + )}\", got: \"0x${marshalled.data.toString("hex")}\"` + ); + return marshalled.data; +} + +function unmarshalTest(t, dptid, jsval, data) { + var dpt = resolve(dptid); + var unmarshalled = fromBuffer(data, dpt); + //console.log('%s: %j --> %j', dpt.id, rhs, converted); + var msg = `${dptid}.fromBuffer(${JSON.stringify( + data + )}) should unmarshall to ${JSON.stringify(jsval)}, got: ${JSON.stringify( + unmarshalled + )}`; + switch (typeof jsval) { + case "object": + t.deepEqual(unmarshalled, jsval, msg); + break; + case "number": + t.equal(unmarshalled, jsval, msg); + break; + default: + t.ok(unmarshalled == jsval, msg); + } +} + +export function run( + dptid: number | string, + tests: { apdu_data: number[]; jsval: any }[] +) { + var dpt = resolve(dptid); + var desc = + (dpt.hasOwnProperty("subtype") && dpt.subtype.desc) || dpt.basetype.desc; + test(`${dptid}: ${desc}`, function (t) { + Object.keys(tests).forEach(function (key) { + var apdu = Buffer.from(tests[key].apdu_data); + var jsval = tests[key].jsval; + //console.log(dptid + ': apdu=%j jsval=%j', apdu, jsval); + // 1. marshalling test (JS value to APDU) + var marshalled_data = marshalTest(t, dptid, jsval, apdu); + // 2. unmarshal from APDU produced by step 1 + unmarshalTest(t, dptid, jsval, marshalled_data); + // 3. unmarshal from test APDU + unmarshalTest(t, dptid, jsval, apdu); + }); + t.end(); + }); +} diff --git a/test/dptlib/test-dpt.js b/test/dptlib/test-dpt.ts similarity index 69% rename from test/dptlib/test-dpt.js rename to test/dptlib/test-dpt.ts index 2d1249c..6ff86a8 100644 --- a/test/dptlib/test-dpt.js +++ b/test/dptlib/test-dpt.ts @@ -3,48 +3,47 @@ * (C) 2016-2018 Elias Karakoulakis */ -const test = require('tape'); -const DPTLib = require('../../src/dptlib'); -const assert = require('assert'); +import test from "tape"; +import { resolve } from "../../src/dptlib"; test('resolve', function(t) { t.throws(() => { - DPTLib.resolve('invalid input'); + resolve('invalid input'); }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); t.throws(() => { - DPTLib.resolve({dpt: 9}); + resolve({dpt: 9} as any); }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); t.throws(() => { - DPTLib.resolve([9,9]); + resolve([9,9] as any); }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); t.throws(() => { - DPTLib.resolve('29.010'); + resolve('29.010'); }, /Unsupported DPT: .*/, 'Unsupported/unknown DPT'); t.throws(() => { - DPTLib.resolve(29); + resolve(29); }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT'); t.throws(() => { - DPTLib.resolve([29]); + resolve([29] as any); }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT'); - var d0 = DPTLib.resolve(1) + var d0 = resolve(1) t.equal(d0.id, 'DPT1') t.equal(d0.subtypeid, undefined) - var d1 = DPTLib.resolve('DPT9') + var d1 = resolve('DPT9') t.equal(d1.id, 'DPT9') t.equal(d1.subtypeid, undefined) - var d2 = DPTLib.resolve('DPT1.002') + var d2 = resolve('DPT1.002') t.equal(d2.id, 'DPT1') t.equal(d2.subtypeid, '002') - var d3 = DPTLib.resolve('DPT1.001') + var d3 = resolve('DPT1.001') t.equal(d3.id, 'DPT1') t.equal(d3.subtypeid, '001') @@ -52,7 +51,7 @@ test('resolve', function(t) { t.equal(d2.id, 'DPT1') t.equal(d2.subtypeid, '002') - var d4 = DPTLib.resolve('1.002') + var d4 = resolve('1.002') t.equal(d4.id, 'DPT1') t.equal(d4.subtypeid, '002') diff --git a/test/dptlib/test-dpt1.js b/test/dptlib/test-dpt1.js deleted file mode 100644 index 78dcbdf..0000000 --- a/test/dptlib/test-dpt1.js +++ /dev/null @@ -1,8 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ -require('./commontest').do('DPT1', [ - { apdu_data: [0x00], jsval: false}, - { apdu_data: [0x01], jsval: true} -]); diff --git a/test/dptlib/test-dpt1.ts b/test/dptlib/test-dpt1.ts new file mode 100644 index 0000000..e175d40 --- /dev/null +++ b/test/dptlib/test-dpt1.ts @@ -0,0 +1,10 @@ +import { run } from "./commontest"; + +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ +run("DPT1", [ + { apdu_data: [0x00], jsval: false }, + { apdu_data: [0x01], jsval: true }, +]); diff --git a/test/dptlib/test-dpt10.js b/test/dptlib/test-dpt10.ts similarity index 82% rename from test/dptlib/test-dpt10.js rename to test/dptlib/test-dpt10.ts index 3276150..47f4e36 100644 --- a/test/dptlib/test-dpt10.js +++ b/test/dptlib/test-dpt10.ts @@ -3,9 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -const test = require('tape'); -const DPTLib = require('../../src/dptlib'); -const assert = require('assert'); +import test from 'tape'; +import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib'; +import { Datagram } from 'src/FSM'; function timecompare(date1, sign, date2) { var dow1 = date1.getDay(); @@ -36,18 +36,18 @@ test('DPT10 time conversion', function(t) { ['DPT10', [(7<<5)+23, 15, 30], new Date('July 7, 2019 23:15:30')] // Sunday ]; for (var i = 0; i < tests.length; i++) { - var dpt = DPTLib.resolve(tests[i][0]); - var buf = new Buffer(tests[i][1]); + var dpt = resolve(tests[i][0] as string); + var buf = Buffer.from(tests[i][1] as number[]); var val = tests[i][2]; // unmarshalling test (raw data to value) - var converted = DPTLib.fromBuffer(buf, dpt); + var converted = fromBuffer(buf, dpt); t.ok(timecompare(converted, '===', val) , `${tests[i][0]} fromBuffer value ${buf.toString('hex')} => expected ${val}, got ${converted}`); // marshalling test (value to raw data) - var apdu = {}; - DPTLib.populateAPDU(val, apdu, 'dpt10'); + var apdu = {} as Datagram["cemi"]["apdu"]; + populateAPDU(val, apdu, 'dpt10'); t.ok(Buffer.compare(buf, apdu.data) == 0, `${tests[i][0]} formatAPDU value ${val} => expected ${buf.toString('hex')}, got ${apdu.data.toString('hex')}`); } diff --git a/test/dptlib/test-dpt11.js b/test/dptlib/test-dpt11.ts similarity index 70% rename from test/dptlib/test-dpt11.js rename to test/dptlib/test-dpt11.ts index 1b0feca..5273185 100644 --- a/test/dptlib/test-dpt11.js +++ b/test/dptlib/test-dpt11.ts @@ -3,11 +3,11 @@ * (C) 2016-2018 Elias Karakoulakis */ -const test = require('tape'); -const DPTLib = require('../../src/dptlib'); -const assert = require('assert'); +import test from 'tape'; +import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib'; +import { Datagram } from 'src/FSM'; -function dateequals(d1, d2) { +function dateequals(d1: Date, d2: Date) { var d = d1.getDate(); var m = d1.getMonth(); var y = d1.getFullYear(); @@ -23,19 +23,19 @@ test('DPT11 date conversion', function(t) { ['DPT11', [0x03, 0x02, 0x13], new Date('2019-02-03')] ] for (var i = 0; i < tests.length; i++) { - var dpt = DPTLib.resolve(tests[i][0]); - var buf = new Buffer(tests[i][1]); - var val = tests[i][2]; + var dpt = resolve(tests[i][0] as string); + var buf = Buffer.from(tests[i][1] as number[]); + var val = tests[i][2] as Date; // unmarshalling test (raw data to value) - var converted = DPTLib.fromBuffer(buf, dpt); + var converted = fromBuffer(buf, dpt); t.ok(dateequals(val, converted), `${tests[i][0]} fromBuffer value ${val} => ${JSON.stringify(converted)}` ); // marshalling test (value to raw data) - var apdu = {}; - DPTLib.populateAPDU(val, apdu, 'dpt11'); + var apdu = {} as Datagram["cemi"]["apdu"]; + populateAPDU(val, apdu, 'dpt11'); t.ok(Buffer.compare(buf, apdu.data) == 0, `${tests[i][0]} formatAPDU value ${val} => ${JSON.stringify(converted)}` ); diff --git a/test/dptlib/test-dpt12.js b/test/dptlib/test-dpt12.ts similarity index 90% rename from test/dptlib/test-dpt12.js rename to test/dptlib/test-dpt12.ts index e4795d8..d2ffd7f 100644 --- a/test/dptlib/test-dpt12.js +++ b/test/dptlib/test-dpt12.ts @@ -3,7 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT12', [ +import { run } from "./commontest"; + +run('DPT12', [ { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17}, { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256}, { apdu_data: [0x00, 0x00, 0x10, 0x01], jsval: 4097}, diff --git a/test/dptlib/test-dpt13.js b/test/dptlib/test-dpt13.ts similarity index 91% rename from test/dptlib/test-dpt13.js rename to test/dptlib/test-dpt13.ts index 0008afe..f1386b8 100644 --- a/test/dptlib/test-dpt13.js +++ b/test/dptlib/test-dpt13.ts @@ -3,7 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT13', [ +import { run } from "./commontest"; + +run('DPT13', [ { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17}, { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256}, { apdu_data: [0x00, 0x00, 0x10, 0x00], jsval: 4096}, diff --git a/test/dptlib/test-dpt19.js b/test/dptlib/test-dpt19.ts similarity index 79% rename from test/dptlib/test-dpt19.js rename to test/dptlib/test-dpt19.ts index a7808ae..d29c4a2 100644 --- a/test/dptlib/test-dpt19.js +++ b/test/dptlib/test-dpt19.ts @@ -3,8 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -const test = require('tape'); -const DPTLib = require('../../src/dptlib'); +import test from 'tape'; +import DPTLib, { fromBuffer, populateAPDU, resolve } from '../../src/dptlib'; +import { Datagram } from 'src/FSM'; test('DPT19 datetime conversion', function(t) { @@ -27,17 +28,17 @@ test('DPT19 datetime conversion', function(t) { ]); var name = 'DPT19'; - var dpt = DPTLib.resolve(name); + var dpt = resolve(name); // unmarshalling test (raw data to value) - var converted = DPTLib.fromBuffer(buffer, dpt); + var converted = fromBuffer(buffer, dpt); t.equal(date.getTime(), converted.getTime(), `${name} fromBuffer value ${JSON.stringify(buffer)} => ${converted}` ); // marshalling test (value to raw data) - var apdu = {}; - DPTLib.populateAPDU(date, apdu, name); + var apdu = {} as Datagram["cemi"]["apdu"]; + populateAPDU(date, apdu, name); t.ok(Buffer.compare(buffer, apdu.data) === 0, `${name} formatAPDU value ${date} => ${JSON.stringify(apdu)}` ); diff --git a/test/dptlib/test-dpt2.js b/test/dptlib/test-dpt2.ts similarity index 86% rename from test/dptlib/test-dpt2.js rename to test/dptlib/test-dpt2.ts index 4d9255a..5b59664 100644 --- a/test/dptlib/test-dpt2.js +++ b/test/dptlib/test-dpt2.ts @@ -3,7 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT2', [ +import { run } from "./commontest"; + +run('DPT2', [ { apdu_data: [0x00], jsval: {priority: 0 , data: 0}}, { apdu_data: [0x01], jsval: {priority: 0 , data: 1}}, { apdu_data: [0x02], jsval: {priority: 1 , data: 0}}, diff --git a/test/dptlib/test-dpt21.js b/test/dptlib/test-dpt21.ts similarity index 91% rename from test/dptlib/test-dpt21.js rename to test/dptlib/test-dpt21.ts index a89b6af..e8dee5c 100644 --- a/test/dptlib/test-dpt21.js +++ b/test/dptlib/test-dpt21.ts @@ -2,9 +2,9 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -const commontest = require('./commontest') +import { run } from "./commontest"; -commontest.do('DPT21', [ +run('DPT21', [ { apdu_data: [0x00], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, { apdu_data: [0x01], jsval: {outofservice: 1, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, { apdu_data: [0x03], jsval: {outofservice: 1, fault: 1, overridden: 0, inalarm: 0, alarmunack: 0}}, @@ -12,7 +12,7 @@ commontest.do('DPT21', [ { apdu_data: [0x08], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 1, alarmunack: 0}} ]); -commontest.do('DPT21.001', [ +run('DPT21.001', [ { apdu_data: [0x01], jsval: {outofservice: 1, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, { apdu_data: [0x05], jsval: {outofservice: 1, fault: 0, overridden: 1, inalarm: 0, alarmunack: 0}}, { apdu_data: [0x06], jsval: {outofservice: 0, fault: 1, overridden: 1, inalarm: 0, alarmunack: 0}}, diff --git a/test/dptlib/test-dpt237.js b/test/dptlib/test-dpt237.ts similarity index 93% rename from test/dptlib/test-dpt237.js rename to test/dptlib/test-dpt237.ts index 4833e9c..c652bfe 100644 --- a/test/dptlib/test-dpt237.js +++ b/test/dptlib/test-dpt237.ts @@ -2,9 +2,9 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -const commontest = require('./commontest') +import { run } from "./commontest"; -commontest.do('DPT237', [ +run('DPT237', [ { apdu_data: [0x00, 0x00], jsval: {address: 0, addresstype: 0, readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, { apdu_data: [0x00, 0x21], jsval: {address: 1, addresstype: 1, @@ -17,7 +17,7 @@ commontest.do('DPT237', [ readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 1}} ]); -commontest.do('DPT237.600', [ +run('DPT237.600', [ { apdu_data: [0x00, 0x01], jsval: {address: 1, addresstype: 0, readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, { apdu_data: [0x00, 0x05], jsval: {address: 5, addresstype: 0, diff --git a/test/dptlib/test-dpt3.js b/test/dptlib/test-dpt3.ts similarity index 82% rename from test/dptlib/test-dpt3.js rename to test/dptlib/test-dpt3.ts index 7b46aee..277008e 100644 --- a/test/dptlib/test-dpt3.js +++ b/test/dptlib/test-dpt3.ts @@ -1,15 +1,16 @@ +import { run } from "./commontest"; + /** * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -const commontest = require('./commontest') -commontest.do('DPT3', [ +run('DPT3', [ { apdu_data: [0x00], jsval: {decr_incr: 0, data: 0}}, { apdu_data: [0x06], jsval: {decr_incr: 0, data: 6}} ]); -commontest.do('DPT3.007', [ +run('DPT3.007', [ { apdu_data: [0x01], jsval: {decr_incr: 0, data: 1}}, { apdu_data: [0x05], jsval: {decr_incr: 0, data: 5}}, { apdu_data: [0x08], jsval: {decr_incr: 1, data: 0}}, diff --git a/test/dptlib/test-dpt4.js b/test/dptlib/test-dpt4.ts similarity index 76% rename from test/dptlib/test-dpt4.js rename to test/dptlib/test-dpt4.ts index 275e156..fe0cd14 100644 --- a/test/dptlib/test-dpt4.js +++ b/test/dptlib/test-dpt4.ts @@ -2,7 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT4', [ + +import { run } from "./commontest"; + +run('DPT4', [ { apdu_data: [0x40], jsval: "@"}, { apdu_data: [0x76], jsval: "v"} ]); diff --git a/test/dptlib/test-dpt5.js b/test/dptlib/test-dpt5.ts similarity index 82% rename from test/dptlib/test-dpt5.js rename to test/dptlib/test-dpt5.ts index b8c7646..3c870a9 100644 --- a/test/dptlib/test-dpt5.js +++ b/test/dptlib/test-dpt5.ts @@ -2,9 +2,11 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -const commontest = require('./commontest'); + +import { run } from "./commontest"; + // DPT5 without subtype: no scaling -commontest.do('DPT5', [ +run('DPT5', [ { apdu_data: [0x00], jsval: 0}, { apdu_data: [0x40], jsval: 64}, { apdu_data: [0x41], jsval: 65}, @@ -12,13 +14,13 @@ commontest.do('DPT5', [ { apdu_data: [0xff], jsval: 255} ]); // 5.001 percentage (0=0..ff=100%) -commontest.do('DPT5.001', [ +run('DPT5.001', [ { apdu_data: [0x00], jsval: 0 }, { apdu_data: [0x80], jsval: 50}, { apdu_data: [0xff], jsval: 100} ]); // 5.003 angle (degrees 0=0, ff=360) -commontest.do('DPT5.003', [ +run('DPT5.003', [ { apdu_data: [0x00], jsval: 0 }, { apdu_data: [0x80], jsval: 181 }, { apdu_data: [0xff], jsval: 360 } diff --git a/test/dptlib/test-dpt6.js b/test/dptlib/test-dpt6.ts similarity index 82% rename from test/dptlib/test-dpt6.js rename to test/dptlib/test-dpt6.ts index 52ccdaf..6d18cb1 100644 --- a/test/dptlib/test-dpt6.js +++ b/test/dptlib/test-dpt6.ts @@ -3,7 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT6', [ +import { run } from "./commontest"; + +run('DPT6', [ { apdu_data: [0x00], jsval: 0}, { apdu_data: [0x7f], jsval: 127}, { apdu_data: [0x80], jsval: -128}, diff --git a/test/dptlib/test-dpt7.js b/test/dptlib/test-dpt7.ts similarity index 84% rename from test/dptlib/test-dpt7.js rename to test/dptlib/test-dpt7.ts index c71d9ef..026c5fd 100644 --- a/test/dptlib/test-dpt7.js +++ b/test/dptlib/test-dpt7.ts @@ -3,7 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT7', [ +import { run } from "./commontest"; + +run('DPT7', [ { apdu_data: [0x00, 0x11], jsval: 17}, { apdu_data: [0x01, 0x00], jsval: 256}, { apdu_data: [0x10, 0x01], jsval: 4097}, diff --git a/test/dptlib/test-dpt8.js b/test/dptlib/test-dpt8.ts similarity index 85% rename from test/dptlib/test-dpt8.js rename to test/dptlib/test-dpt8.ts index 4037692..c2c60d3 100644 --- a/test/dptlib/test-dpt8.js +++ b/test/dptlib/test-dpt8.ts @@ -3,7 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT8', [ +import { run } from "./commontest"; + +run('DPT8', [ { apdu_data: [0x00, 0x11], jsval: 17}, { apdu_data: [0x01, 0x00], jsval: 256}, { apdu_data: [0x7f, 0xff], jsval: 32767}, diff --git a/test/dptlib/test-dpt9.js b/test/dptlib/test-dpt9.ts similarity index 90% rename from test/dptlib/test-dpt9.js rename to test/dptlib/test-dpt9.ts index 9f0adb8..b0f8453 100644 --- a/test/dptlib/test-dpt9.js +++ b/test/dptlib/test-dpt9.ts @@ -3,7 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -require('./commontest').do('DPT9', [ +import { run } from "./commontest"; + +run('DPT9', [ { apdu_data: [0x00, 0x02], jsval: 0.02}, { apdu_data: [0x87, 0xfe], jsval: -0.02}, { apdu_data: [0x02, 0xf8], jsval: 7.6}, diff --git a/test/knxproto/test-address.js b/test/knxproto/test-address.ts similarity index 71% rename from test/knxproto/test-address.js rename to test/knxproto/test-address.ts index 902ee7c..44165f9 100644 --- a/test/knxproto/test-address.js +++ b/test/knxproto/test-address.ts @@ -3,20 +3,19 @@ * (C) 2016-2018 Elias Karakoulakis */ -const address = require('../../src/Address.js'); -const assert = require('assert'); -const test = require('tape'); +import * as address from '../../src/Address'; +import test from 'tape'; // test('KNX physical address test', function(t) { var tests = { - "0.0.0": new Buffer([0, 0]), - "0.0.10": new Buffer([0, 10]), - "0.0.255": new Buffer([0, 255]), - "0.1.0": new Buffer([1, 0]), - "1.0.0": new Buffer([16, 0]), - "15.14.0": new Buffer([254, 0]), - "15.15.0": new Buffer([255, 0]), + "0.0.0": Buffer.from([0, 0]), + "0.0.10": Buffer.from([0, 10]), + "0.0.255": Buffer.from([0, 255]), + "0.1.0": Buffer.from([1, 0]), + "1.0.0": Buffer.from([16, 0]), + "15.14.0": Buffer.from([254, 0]), + "15.15.0": Buffer.from([255, 0]), }; Object.keys(tests).forEach((key, idx) => { var buf = tests[key]; @@ -39,14 +38,14 @@ test('KNX physical address test', function(t) { // test('KNX group address test', function(t) { var tests = { - "0/0/0": new Buffer([0, 0]), - "0/0/10": new Buffer([0, 10]), - "0/0/255": new Buffer([0, 255]), - "0/1/0": new Buffer([1, 0]), - "1/0/0": new Buffer([8, 0]), - "1/7/0": new Buffer([15, 0]), - "31/6/0": new Buffer([254, 0]), - "31/7/0": new Buffer([255, 0]), + "0/0/0": Buffer.from([0, 0]), + "0/0/10": Buffer.from([0, 10]), + "0/0/255": Buffer.from([0, 255]), + "0/1/0": Buffer.from([1, 0]), + "1/0/0": Buffer.from([8, 0]), + "1/7/0": Buffer.from([15, 0]), + "31/6/0": Buffer.from([254, 0]), + "31/7/0": Buffer.from([255, 0]), }; Object.keys(tests).forEach((key, idx) => { var buf = tests[key]; diff --git a/test/knxproto/test-proto.js b/test/knxproto/test-proto.ts similarity index 93% rename from test/knxproto/test-proto.js rename to test/knxproto/test-proto.ts index 9c8f5db..bde2582 100644 --- a/test/knxproto/test-proto.js +++ b/test/knxproto/test-proto.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -const knxnetprotocol = require('../../src/KnxProtocol.js'); -const assert = require('assert'); -const test = require('tape'); -knxnetprotocol.debug = true; +import KnxNetProtocol from '../../src/KnxProtocol'; +import test from 'tape'; + +KnxNetProtocol.debug = true; // test('KNX protocol unmarshaller', function(t) { @@ -20,8 +20,8 @@ test('KNX protocol unmarshaller', function(t) { Object.keys(tests).forEach((key, idx) => { var buf = tests[key]; // unmarshal from a buffer... - var reader = knxnetprotocol.createReader(buf); - var writer = knxnetprotocol.createWriter(); + var reader = KnxNetProtocol.createReader(buf); + var writer = KnxNetProtocol.createWriter(); reader.KNXNetHeader('tmp'); var decoded = reader.next()['tmp']; console.log("\n=== %s: %j ===> %j", key, buf, decoded); @@ -54,8 +54,8 @@ test('KNX protocol marshal+unmarshal', function(t) { Object.keys(tests).forEach((key, idx) => { var buf = tests[key]; // unmarshal from a buffer... - var reader = knxnetprotocol.createReader(buf); - var writer = knxnetprotocol.createWriter(); + var reader = KnxNetProtocol.createReader(buf); + var writer = KnxNetProtocol.createWriter(); reader.KNXNetHeader('tmp'); var decoded = reader.next()['tmp']; console.log("\n=== %s: %j ===> %j", key, buf, decoded); @@ -238,14 +238,14 @@ test('KNX protocol marshaller', function(t) { Object.keys(tests).forEach((key, idx) => { var testcase = tests[key]; var buf = typeof testcase.hexbuf == 'string' ? - new Buffer(testcase.hexbuf.replace(/\s/g, ''), 'hex') : hexbuf; + Buffer.from(testcase.hexbuf.replace(/\s/g, ''), 'hex') : testcase.hexbuf; console.log("\n=== %s", key); // marshal the test datagram - var writer = knxnetprotocol.createWriter(); + var writer = KnxNetProtocol.createWriter(); writer.KNXNetHeader(testcase.dgram); if (Buffer.compare(buf, writer.buffer) != 0) { // if this fails, unmarshal the buffer again to a datagram - var reader = knxnetprotocol.createReader(writer.buffer); + var reader = KnxNetProtocol.createReader(writer.buffer); reader.KNXNetHeader('tmp'); var decoded = reader.next()['tmp']; console.log("\n\n========\n FAIL: %s\n========\nbuffer is different:\n", key); diff --git a/test/wiredtests/test-connect-routing-hybrid.js b/test/wiredtests/test-connect-routing-hybrid.ts similarity index 79% rename from test/wiredtests/test-connect-routing-hybrid.js rename to test/wiredtests/test-connect-routing-hybrid.ts index d8eb7ab..dbeb42c 100644 --- a/test/wiredtests/test-connect-routing-hybrid.js +++ b/test/wiredtests/test-connect-routing-hybrid.ts @@ -4,12 +4,8 @@ */ Error.stackTraceLimit = Infinity; -const knx = require('../..'); -const address = require('../../src/Address.js'); -const assert = require('assert'); -const test = require('tape'); - -const options = require('./wiredtest-options.js'); +import { Connection, LogLevel } from '../../src'; +import test from 'tape'; /* ========== ================== @@ -21,8 +17,8 @@ const options = require('./wiredtest-options.js'); if (process.env.hasOwnProperty('WIREDTEST')) { // test('KNX connect routing hybrid', function(t) { - var connection = knx.Connection({ - loglevel: 'debug', + var connection = new Connection({ + loglevel: LogLevel.Debug, forceTunneling: true, handlers: { connected: function() { diff --git a/test/wiredtests/test-connect-tunnel.js b/test/wiredtests/test-connect-tunnel.ts similarity index 85% rename from test/wiredtests/test-connect-tunnel.js rename to test/wiredtests/test-connect-tunnel.ts index bbd1354..29d3e9b 100644 --- a/test/wiredtests/test-connect-tunnel.js +++ b/test/wiredtests/test-connect-tunnel.ts @@ -5,12 +5,10 @@ Error.stackTraceLimit = Infinity; -const knx = require('../..'); -const address = require('../../src/Address.js'); -const assert = require('assert'); -const test = require('tape'); +import { Connection } from '../../src'; +import test from 'tape'; -const options = require('./wiredtest-options.js'); +import options from './wiredtest-options'; /* ========== ================== @@ -22,7 +20,7 @@ const options = require('./wiredtest-options.js'); if (process.env.hasOwnProperty('WIREDTEST')) { // test('KNX connect tunneling', function(t) { - var connection = knx.Connection({ + var connection = new Connection({ // set up your KNX IP router's IP address (not multicast!) // for getting into tunnelling mode ipAddr: options.ipAddr, diff --git a/test/wiredtests/test-control.js b/test/wiredtests/test-control.ts similarity index 89% rename from test/wiredtests/test-control.js rename to test/wiredtests/test-control.ts index 44ce019..2a4337e 100644 --- a/test/wiredtests/test-control.js +++ b/test/wiredtests/test-control.ts @@ -4,12 +4,10 @@ */ Error.stackTraceLimit = Infinity; -const knx = require('../..'); -const address = require('../../src/Address.js'); -const assert = require('assert'); -const test = require('tape'); +import { Connection, Datapoint } from '../../src'; +import test from "tape"; -const options = require('./wiredtest-options.js'); +import options from "./wiredtest-options"; /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN @@ -21,7 +19,7 @@ if (process.env.hasOwnProperty('WIREDTEST')) { test('KNX wired test - control a basic DPT1 binary switch', function(t) { var counter = 0; - var connection = new knx.Connection({ + var connection = new Connection({ debug: true, physAddr: options.physAddr, handlers: { @@ -29,7 +27,7 @@ if (process.env.hasOwnProperty('WIREDTEST')) { console.log('----------'); console.log('Connected!'); console.log('----------'); - var light = new knx.Datapoint({ + var light = new Datapoint({ ga: options.wired_test_control_ga, dpt: 'DPT1.001' }, connection); diff --git a/test/wiredtests/test-logotimer.js b/test/wiredtests/test-logotimer.ts similarity index 70% rename from test/wiredtests/test-logotimer.js rename to test/wiredtests/test-logotimer.ts index 87e9625..050ca83 100644 --- a/test/wiredtests/test-logotimer.js +++ b/test/wiredtests/test-logotimer.ts @@ -2,11 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ - -const knx = require('../..'); -const test = require('tape'); -const util = require('util'); -const options = require('./wiredtest-options.js'); +import { Connection, Datapoint } from '../../src'; +import test from 'tape'; +import util from 'util'; +import options from './wiredtest-options'; /* ========== ================== @@ -17,12 +16,12 @@ const options = require('./wiredtest-options.js'); */ if (process.env.hasOwnProperty('WIREDTEST')) { test('KNX wired test - control a DPT9 timer', function(t) { - var connection = new knx.Connection( { + var connection = new Connection( { //debug: true, handlers: { connected: () => { - var timer_control = new knx.Datapoint({ga: options.dpt9_timer_control_ga, dpt: 'DPT9.001', autoread: true}, connection); - var timer_status = new knx.Datapoint({ga: options.dpt9_timer_status_ga, dpt: 'DPT9.001', autoread: true}, connection); + var timer_control = new Datapoint({ga: options.dpt9_timer_control_ga, dpt: 'DPT9.001', autoread: true}, connection); + var timer_status = new Datapoint({ga: options.dpt9_timer_status_ga, dpt: 'DPT9.001', autoread: true}, connection); timer_control.on('change', function(oldvalue, newvalue) { t.pass(util.format("**** Timer control changed from: %j to: %j", oldvalue, newvalue)); }); diff --git a/test/wiredtests/test-read.js b/test/wiredtests/test-read.ts similarity index 85% rename from test/wiredtests/test-read.js rename to test/wiredtests/test-read.ts index 993fc0d..b687cff 100644 --- a/test/wiredtests/test-read.js +++ b/test/wiredtests/test-read.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -const knx = require('../..'); -const test = require('tape'); -const util = require('util'); -const options = require('./wiredtest-options.js'); +import { Connection, Datapoint } from '../../src'; +import test from 'tape'; +import util from 'util'; +import options from './wiredtest-options'; /* ========== ================== @@ -17,13 +17,13 @@ const options = require('./wiredtest-options.js'); */ if (process.env.hasOwnProperty('WIREDTEST')) { test('KNX wired test - read a temperature', function(t) { - var connection = new knx.Connection({ + var connection = new Connection({ debug: true, physAddr: options.physAddr, handlers: { connected: function() { // just define a temperature GA that should respond to a a GroupValue_Read request - var temperature_in = new knx.Datapoint({ + var temperature_in = new Datapoint({ ga: options.dpt9_temperature_status_ga, dpt: 'DPT9.001' }, connection); diff --git a/test/wiredtests/test-readstorm.js b/test/wiredtests/test-readstorm.js deleted file mode 100644 index 145163d..0000000 --- a/test/wiredtests/test-readstorm.js +++ /dev/null @@ -1,77 +0,0 @@ -/** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -const knx = require('../..'); -const test = require('tape'); -const util = require('util'); -const options = require('./wiredtest-options.js'); - -Error.stackTraceLimit = Infinity; - -/* - ========== ================== - this is a WIRED test and requires a real KNX IP router on the LAN - ========== ================== - to run all tests: $ WIREDTEST=1 npm test - to run one test : $ WIREDTEST=1 node test/wiredtests/.js -*/ -if (process.env.hasOwnProperty('WIREDTEST')) { - test('KNX wired test - read multiple statuses from a consecutive GA range', function(t) { - var readback = {}; - function setupDatapoint(groupadress, statusga) { - var dp = new knx.Datapoint({ - ga: groupadress, - status_ga: statusga, - dpt: "DPT1.001", - autoread: true - }, connection); - dp.on('change', (oldvalue, newvalue) => { - console.log("**** %s current value: %j", groupadress, newvalue); - }); - return dp; - } - function setupDatapoints() { - var ctrl_ga_arr = options.readstorm_control_ga_start.split('/'); - var stat_ga_arr = options.readstorm_status_ga_start.split('/'); - for (i=0; i< options.readstorm_range; i++) { - var ctrl_ga = [ctrl_ga_arr[0], ctrl_ga_arr[1], i+parseInt(ctrl_ga_arr[2])].join('/'); - var stat_ga = [stat_ga_arr[0], stat_ga_arr[1], i+parseInt(stat_ga_arr[2])].join('/'); - setupDatapoint(ctrl_ga, stat_ga); - } - } - var connection = knx.Connection({ - loglevel: 'warn', - //forceTunneling: true, -// minimumDelay: 100, - handlers: { - connected: function() { - setupDatapoints(); - }, - event: function (evt, src, dest, value) { - if (evt == 'GroupValue_Response') { - readback[dest] = [src, value]; - // have we got responses from all the read requests for all datapoints? - if (Object.keys(readback).length == options.readstorm_range) { - t.pass(util.format('readstorm: all %d datapoints accounted for', options.readstorm_range)); - t.end(); - process.exit(0); - } - } - }, - error: function(connstatus) { - console.log("%s **** ERROR: %j", - new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), - connstatus); - process.exit(1); - } - } - }); - }); - - setTimeout(function() { - console.log('Exiting with timeout...'); - process.exit(2); - }, 1500); -} diff --git a/test/wiredtests/test-readstorm.ts b/test/wiredtests/test-readstorm.ts new file mode 100644 index 0000000..559c8ba --- /dev/null +++ b/test/wiredtests/test-readstorm.ts @@ -0,0 +1,95 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { Connection, Datapoint, LogLevel } from "../../src"; +import test from "tape"; +import util from "util"; +import options from "./wiredtest-options.js"; + +Error.stackTraceLimit = Infinity; + +/* + ========== ================== + this is a WIRED test and requires a real KNX IP router on the LAN + ========== ================== + to run all tests: $ WIREDTEST=1 npm test + to run one test : $ WIREDTEST=1 node test/wiredtests/.js +*/ +if (process.env.hasOwnProperty("WIREDTEST")) { + test("KNX wired test - read multiple statuses from a consecutive GA range", function (t) { + var readback = {}; + function setupDatapoint(groupadress: string, statusga: string) { + var dp = new Datapoint( + { + ga: groupadress, + status_ga: statusga, + dpt: "DPT1.001", + autoread: true, + }, + connection + ); + dp.on("change", (oldvalue, newvalue) => { + console.log("**** %s current value: %j", groupadress, newvalue); + }); + return dp; + } + function setupDatapoints() { + var ctrl_ga_arr = options.readstorm_control_ga_start.split("/"); + var stat_ga_arr = options.readstorm_status_ga_start.split("/"); + for (let i = 0; i < options.readstorm_range; i++) { + var ctrl_ga = [ + ctrl_ga_arr[0], + ctrl_ga_arr[1], + i + parseInt(ctrl_ga_arr[2]), + ].join("/"); + var stat_ga = [ + stat_ga_arr[0], + stat_ga_arr[1], + i + parseInt(stat_ga_arr[2]), + ].join("/"); + setupDatapoint(ctrl_ga, stat_ga); + } + } + var connection = new Connection({ + loglevel: LogLevel.Warn, + //forceTunneling: true, + // minimumDelay: 100, + handlers: { + connected: function () { + setupDatapoints(); + }, + event: function (evt, src, dest, value) { + if (evt == "GroupValue_Response") { + readback[dest] = [src, value]; + // have we got responses from all the read requests for all datapoints? + if (Object.keys(readback).length == options.readstorm_range) { + t.pass( + util.format( + "readstorm: all %d datapoints accounted for", + options.readstorm_range + ) + ); + t.end(); + process.exit(0); + } + } + }, + error: function (connstatus) { + console.log( + "%s **** ERROR: %j", + new Date().toISOString().replace(/T/, " ").replace(/Z$/, ""), + connstatus + ); + process.exit(1); + }, + }, + }); + }); + + setTimeout(function () { + console.log("Exiting with timeout..."); + process.exit(2); + }, 1500); +} diff --git a/test/wiredtests/wiredtest-options.js b/test/wiredtests/wiredtest-options.ts similarity index 95% rename from test/wiredtests/wiredtest-options.js rename to test/wiredtests/wiredtest-options.ts index 26620bc..e3abd2d 100644 --- a/test/wiredtests/wiredtest-options.js +++ b/test/wiredtests/wiredtest-options.ts @@ -6,7 +6,7 @@ /* define the required options for running wired tests */ -module.exports = { + const options = { loglevel: 'trace', // your KNX IP router UNICAST ip address ipAddr: '192.168.8.4', @@ -27,3 +27,5 @@ module.exports = { readstorm_status_ga_start: '1/1/100', readstorm_range: 8 } + +export default options; From b6696b7c40fe10a8ad00b3b8bc327cee91e4a08a Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 10:36:11 +0200 Subject: [PATCH 04/36] fix: replace deprecated new Buffer --- src/dptlib/dpt18.ts | 2 +- src/dptlib/dpt21.ts | 2 +- test/dptlib/test-dpt19.ts | 2 +- test/knxproto/test-proto.ts | 18 +++++++++--------- test/wiredtests/test-control.ts | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts index 336021e..2394863 100644 --- a/src/dptlib/dpt18.ts +++ b/src/dptlib/dpt18.ts @@ -32,7 +32,7 @@ const config: DatapointConfig = { formatAPDU: function (value) { if (value == null) log.warn("DPT18: cannot write null value"); else { - var apdu_data = new Buffer(1); + var apdu_data = Buffer.alloc(1); if ( typeof value == "object" && value.hasOwnProperty("save_recall") && diff --git a/src/dptlib/dpt21.ts b/src/dptlib/dpt21.ts index 73c7499..8803043 100644 --- a/src/dptlib/dpt21.ts +++ b/src/dptlib/dpt21.ts @@ -24,7 +24,7 @@ const config: DatapointConfig = { if (value == null) return log.error("DPT21: cannot write null value"); log.debug("./knx/src/dpt21.js : input value = " + value); - //var apdu_data = new Buffer(1); + //var apdu_data = Buffer.alloc(1); //apdu_data[0] = value; if (typeof value === "object") return Buffer.from([ diff --git a/test/dptlib/test-dpt19.ts b/test/dptlib/test-dpt19.ts index d29c4a2..4c05a49 100644 --- a/test/dptlib/test-dpt19.ts +++ b/test/dptlib/test-dpt19.ts @@ -16,7 +16,7 @@ test('DPT19 datetime conversion', function(t) { date.setMilliseconds(0); var day = (date.getDay() === 0) ? 7 : date.getDay(); - var buffer = new Buffer([ + var buffer = Buffer.from([ date.getFullYear() - 1900, // year with offset 1900 date.getMonth() + 1, // month from 1 - 12 date.getDate(), // day of month from 1 - 31 diff --git a/test/knxproto/test-proto.ts b/test/knxproto/test-proto.ts index bde2582..500d83a 100644 --- a/test/knxproto/test-proto.ts +++ b/test/knxproto/test-proto.ts @@ -11,7 +11,7 @@ KnxNetProtocol.debug = true; // test('KNX protocol unmarshaller', function(t) { var tests = { - "ETS5 programming request": new Buffer([ + "ETS5 programming request": Buffer.from([ 6, 16, 4, 32, 0, 20, 4, 34, 1, 0, 197, 0, 17, 252, 17, 253, 17, 1, 0, 238 @@ -32,21 +32,21 @@ test('KNX protocol unmarshaller', function(t) { test('KNX protocol marshal+unmarshal', function(t) { var tests = { - CONNECT_REQUEST: new Buffer( + CONNECT_REQUEST: Buffer.from( "06100205001a0801c0a80ab3d96d0801c0a80ab3d83604040200", 'hex'), - CONNECT_RESPONSE: new Buffer( + CONNECT_RESPONSE: Buffer.from( "061002060014030008010a0c17350e5704040000", 'hex'), - "CONNECT_RESPONSE, failure E_NO_MORE_CONNECTIONS: 0x24": new Buffer( + "CONNECT_RESPONSE, failure E_NO_MORE_CONNECTIONS: 0x24": Buffer.from( "0610020600080024", 'hex'), - "tunneling request (GroupValue_Read) apdu=1byte": new Buffer( + "tunneling request (GroupValue_Read) apdu=1byte": Buffer.from( "061004200015040200002e00bce000000832010000", 'hex'), - "tunneling request (GroupValue_Write) apdu=1byte": new Buffer( + "tunneling request (GroupValue_Write) apdu=1byte": Buffer.from( "061004200015040200002e00bce000000832010081", 'hex'), - "tunneling request (GroupValue_Write) apdu=2byte": new Buffer( + "tunneling request (GroupValue_Write) apdu=2byte": Buffer.from( "061004200016040201002900bce00000083b0200804a", 'hex'), - "routing indication": new Buffer( + "routing indication": Buffer.from( "0610053000112900bce0ff0f0908010000", 'hex'), - DISCONNECT_REQUEST: new Buffer([ + DISCONNECT_REQUEST: Buffer.from([ 6, 16, 2, 9, 0, 16, 142, 142, 8, 1, 192, 168, 2, 222, 14, 87 ]), diff --git a/test/wiredtests/test-control.ts b/test/wiredtests/test-control.ts index 2a4337e..b0fff20 100644 --- a/test/wiredtests/test-control.ts +++ b/test/wiredtests/test-control.ts @@ -49,11 +49,11 @@ if (process.env.hasOwnProperty('WIREDTEST')) { connection.write('10/10/5', 1); // operation 3 - Do the same with writeRaw setTimeout(function() { - connection.writeRaw(options.wired_test_control_ga, new Buffer('00', 'hex'), 1); + connection.writeRaw(options.wired_test_control_ga, Buffer.from('00', 'hex'), 1); }, 1000); // operation 4 - Do the same with writeRaw setTimeout(function() { - connection.writeRaw(options.wired_test_control_ga, new Buffer('01', 'hex'), 0); + connection.writeRaw(options.wired_test_control_ga, Buffer.from('01', 'hex'), 0); }, 1500); }, event: function(evt, src, dest, value) { From fb98cbdb9581d02cfe075c83432c7868ef90fa7e Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 11:48:15 +0200 Subject: [PATCH 05/36] fix: add missing types --- index.d.ts | 119 ------------------ package.json | 2 +- src/FSM.ts | 20 +-- src/IpRoutingConnection.ts | 4 +- src/KnxLog.ts | 26 +--- src/KnxProtocol.ts | 2 +- src/index.ts | 8 +- src/types/binary-protocol.d.ts | 2 +- src/types/log-driver.d.ts | 21 ++++ src/types/machina.d.ts | 2 +- test/connection/test-connect-routing.ts | 4 +- .../wiredtests/test-connect-routing-hybrid.ts | 4 +- test/wiredtests/test-readstorm.ts | 4 +- 13 files changed, 50 insertions(+), 168 deletions(-) delete mode 100644 index.d.ts create mode 100644 src/types/log-driver.d.ts diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index ebb2f91..0000000 --- a/index.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -/// - -import * as events from "events"; - -type HandlersSpec = { - connected?: () => void; - disconnected?: () => void; - event?: ( - evt: string, - src: KnxDeviceAddress, - dest: KnxGroupAddress, - value: Buffer - ) => void; - error?: (connstatus: any) => void; -}; - -type ConnectionSpec = { - /** ip address of the KNX router or interface */ - ipAddr?: string; - /** port of the KNX router or interface */ - ipPort?: number; - /** in case you need to specify the multicast interface (say if you have more than one) */ - interface?: string; - /** the KNX physical address we'd like to use */ - physAddr?: string; - /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ - loglevel?: string; - /** do not automatically connect, but use connection.Connect() to establish connection */ - manualConnect?: boolean; - /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ - forceTunneling?: boolean; - /** wait at least 10 millisec between each datagram */ - minimumDelay?: number; - /** enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this */ - suppress_ack_ldatareq?: boolean; - /** 14/03/2020 In tunneling mode, echoes the sent message by emitting a new emitEvent, so other object with same group address, can receive the sent message. Default is false. */ - localEchoInTunneling?: boolean; - /** event handlers. You can also bind them later with connection.on(event, fn) */ - handlers?: HandlersSpec; -}; - -type KnxDeviceAddress = string; - -type KnxGroupAddress = string; - -/** The type of the KnxValue depends on the DPT that it is associated with */ -type KnxValue = number | string | boolean | Date; - -/** Possible formats "X" or "X.Y", i.e. "1" or "1.001" */ -type DPT = string; - -type DatapointOptions = { - ga: KnxGroupAddress; - dpt?: DPT; - autoread?: boolean; -}; - -interface DatapointEvent { - on( - event: "change", - listener: (old_value: KnxValue, new_value: KnxValue) => void - ): this; - on(event: string, listener: (event: string, value: any) => void): this; -} - -declare module "knx" { - type MachinaEventsCallback = (...args: any[]) => void; - - interface MachinaEventsReturn { - eventName: string; - callback: MachinaEventsCallback; - off: () => void; - } - - class MachinaEvents { - emit(eventName: string): void; - on(eventName: string, callback: MachinaEventsCallback): MachinaEventsReturn; - off(eventName?: string, callback?: MachinaEventsCallback): void; - } - - interface MachinaEventsReturn { - eventName: string; - callback: MachinaEventsCallback; - off: () => void; - } - - export interface IConnection extends MachinaEvents { - debug: boolean; - Connect(): void; - Disconnect(cb?: () => void): void; - read(ga: KnxGroupAddress, cb?: (src: KnxDeviceAddress, value: Buffer) => void): void; - write(ga: KnxGroupAddress, value: Buffer, dpt: DPT, cb?: () => void): void; - } - - export class Connection extends MachinaEvents implements IConnection { - public debug: boolean; - constructor(conf: ConnectionSpec); - Connect(): void; - Disconnect(cb?: () => void): void; - read(ga: KnxGroupAddress, cb?: (src: KnxDeviceAddress, value: Buffer) => void): void; - write(ga: KnxGroupAddress, value: Buffer, dpt: DPT, cb?: () => void): void; - writeRaw( - ga: KnxGroupAddress, - value: Buffer, - bitlength?: number, - cb?: () => void - ): void; - } - - export class Datapoint extends events.EventEmitter implements DatapointEvent { - readonly current_value: KnxValue; - readonly dptid: DPT; - - constructor(options: DatapointOptions, conn?: IConnection); - bind(conn: Connection): void; - write(value: KnxValue): void; - read(callback?: (src: KnxDeviceAddress, value: KnxValue) => void): void; - } -} diff --git a/package.json b/package.json index 0040264..a51acb7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "scripts": { "build": "tsc -p tsconfig.build.json", - "test": "multi-tape --node-arg='-r esbuild-register' test/*/test-*.ts" + "test": "node -r esbuild-register ./node_modules/.bin/multi-tape test/*/test-*.ts" }, "main": "./build/index.js", "license": "MIT", diff --git a/src/FSM.ts b/src/FSM.ts index aaefb6d..54455b1 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -1,18 +1,20 @@ import os from "os"; import util from "util"; import * as ipaddr from "ipaddr.js"; -import * as machina from "machina"; -import { keyText, KnxConstants } from "./KnxConstants.js"; -import IpRoutingConnection from "./IpRoutingConnection.js"; -import IpTunnelingConnection from "./IpTunnelingConnection.js"; -import KnxLog, { KnxLogOptions } from "./KnxLog.js"; +import machina from "machina"; +import { keyText, KnxConstants } from "./KnxConstants"; +import IpRoutingConnection from "./IpRoutingConnection"; +import IpTunnelingConnection from "./IpTunnelingConnection"; +import KnxLog, { KnxLogOptions } from "./KnxLog"; import KnxNetProtocol from "./KnxProtocol"; import { Writer } from "binary-protocol"; import { Socket } from "dgram"; -import { populateAPDU } from "./dptlib/index.js"; +import { populateAPDU } from "./dptlib"; +import { LogLevel } from "log-driver"; type KnxDeviceAddress = string; + type KnxGroupAddress = string; /** The type of the KnxValue depends on the DPT that it is associated with */ @@ -40,7 +42,7 @@ export type KnxOptions = { /** the KNX physical address we'd like to use */ physAddr?: string; /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ - loglevel?: string; + loglevel?: LogLevel; /** do not automatically connect, but use connection.Connect() to establish connection */ manualConnect?: boolean; /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ @@ -113,7 +115,7 @@ export interface Datagram { }; } -export class KnxFSM extends machina.FSM { +export class KnxFSM extends machina.Fsm { private options: KnxOptions; private log: any; private ThreeLevelGroupAddressing: boolean; @@ -1074,6 +1076,6 @@ const AddTunn = (datagram: Datagram): void => { }; }; -const KnxFSMConnection = machina.FSM.extend(KnxFSM) +const KnxFSMConnection = machina.Fsm.extend(KnxFSM) export default KnxFSMConnection; diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index 716ca5a..dede8bf 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -1,7 +1,7 @@ import * as util from 'util'; import * as dgram from 'dgram'; -import KnxLog from './KnxLog.js'; -import { KnxFSM } from './FSM.js'; +import KnxLog from './KnxLog'; +import { KnxFSM } from './FSM'; function IpRoutingConnection(instance: KnxFSM): KnxFSM { const log = KnxLog.get(); diff --git a/src/KnxLog.ts b/src/KnxLog.ts index b97a8ac..d070b7c 100644 --- a/src/KnxLog.ts +++ b/src/KnxLog.ts @@ -1,35 +1,13 @@ import util from 'util'; -import factory from 'log-driver'; +import factory, { Logger, LogLevel } from 'log-driver'; -export enum LogLevel { - Trace = 'trace', - Debug = 'debug', - Info = 'info', - Warn = 'warn', - Error = 'error', -} - -export interface LogDriverOptions { - level: LogLevel; - format: (level: LogLevel, msg: string, ...args: any[]) => string; -} export interface KnxLogger { get: (options?: KnxLogOptions) => Logger; } -export interface Logger { - trace: (...args: any[]) => void; - debug: (...args: any[]) => void; - info: (...args: any[]) => void; - warn: (...args: any[]) => void; - error: (...args: any[]) => void; - format: LogDriverOptions['format']; -} - let logger: Logger; - export interface KnxLogOptions { debug?: boolean; loglevel?: LogLevel; @@ -37,7 +15,7 @@ export interface KnxLogOptions { const create = (options: KnxLogOptions): Logger => { const level: LogLevel = - (options && (options.debug ? LogLevel.Debug : options.loglevel)) || LogLevel.Info; + (options && (options.debug ? 'debug' : options.loglevel)) || 'info'; return factory({ level, format(lvl: LogLevel, msg: string, ...a: any[]) { diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index 80643b8..17c4587 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -6,7 +6,7 @@ import util from "util"; import ipaddr from "ipaddr.js"; import { Parser } from 'binary-parser' -import { BinaryProtocol } from "binary-protocol"; +import BinaryProtocol from "binary-protocol"; import * as KnxAddress from "./Address"; import { APCICODES, keyText, KnxConstants } from "./KnxConstants"; import KnxLog from "./KnxLog"; diff --git a/src/index.ts b/src/index.ts index 8983350..69c15c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,16 +4,16 @@ */ import { format } from 'util'; -import log from 'log-driver'; +import { logger } from 'log-driver'; import Connection from './FSM'; import Datapoint from './Datapoint'; import Devices from './devices'; -import Log, { LogLevel } from './KnxLog'; +import Log from './KnxLog'; const pkgJson = require('../package.json'); -log.info(format('Loading %s: %s, version: %s', +logger.info(format('Loading %s: %s, version: %s', pkgJson.name, pkgJson.description, pkgJson.version)); -export { Connection, Datapoint, Devices, Log, LogLevel }; \ No newline at end of file +export { Connection, Datapoint, Devices, Log }; \ No newline at end of file diff --git a/src/types/binary-protocol.d.ts b/src/types/binary-protocol.d.ts index c1bbee8..2adecc9 100644 --- a/src/types/binary-protocol.d.ts +++ b/src/types/binary-protocol.d.ts @@ -45,7 +45,7 @@ declare module "binary-protocol" { createWriteStream(options: any): Writer; } - export class BinaryProtocol { + export default class BinaryProtocol { define(name: string, config: ProtocolConfig): this; createReader(buffer: Buffer, offset?: number): Reader; createWriter(buffer?: Buffer, offset?: number): Writer; diff --git a/src/types/log-driver.d.ts b/src/types/log-driver.d.ts new file mode 100644 index 0000000..0bd545e --- /dev/null +++ b/src/types/log-driver.d.ts @@ -0,0 +1,21 @@ +declare module "log-driver" { + export type LogLevel = "trace" | "debug" | "info" | "warn" | "error"; + + export interface LogDriverOptions { + level: LogLevel; + format: (level: LogLevel, msg: string, ...args: any[]) => string; + } + + export interface Logger { + trace: (...args: any[]) => void; + debug: (...args: any[]) => void; + info: (...args: any[]) => void; + warn: (...args: any[]) => void; + error: (...args: any[]) => void; + format: LogDriverOptions["format"]; + } + + export default function factory(options: LogDriverOptions): Logger; + + export const logger: Logger; +} diff --git a/src/types/machina.d.ts b/src/types/machina.d.ts index 2947fbd..2f34831 100644 --- a/src/types/machina.d.ts +++ b/src/types/machina.d.ts @@ -41,7 +41,7 @@ declare module "machina" { transition(client: BehavioralFsm, newState: string): void; } - export class FSM extends BehavioralFsm { + export class Fsm extends BehavioralFsm { compositeState(): any; clearQueue(name?: string): void; handle(...args: any[]): any; diff --git a/test/connection/test-connect-routing.ts b/test/connection/test-connect-routing.ts index 2e4d54f..2959a93 100644 --- a/test/connection/test-connect-routing.ts +++ b/test/connection/test-connect-routing.ts @@ -5,13 +5,13 @@ Error.stackTraceLimit = Infinity; -import { Connection, LogLevel } from '../../src'; +import { Connection } from '../../src'; import test from 'tape'; // test('KNX connect routing', function(t) { var connection = new Connection({ - loglevel: LogLevel.Trace, + loglevel: 'trace', handlers: { connected: function() { console.log('----------'); diff --git a/test/wiredtests/test-connect-routing-hybrid.ts b/test/wiredtests/test-connect-routing-hybrid.ts index dbeb42c..ee92537 100644 --- a/test/wiredtests/test-connect-routing-hybrid.ts +++ b/test/wiredtests/test-connect-routing-hybrid.ts @@ -4,7 +4,7 @@ */ Error.stackTraceLimit = Infinity; -import { Connection, LogLevel } from '../../src'; +import { Connection } from '../../src'; import test from 'tape'; /* @@ -18,7 +18,7 @@ if (process.env.hasOwnProperty('WIREDTEST')) { // test('KNX connect routing hybrid', function(t) { var connection = new Connection({ - loglevel: LogLevel.Debug, + loglevel: 'debug', forceTunneling: true, handlers: { connected: function() { diff --git a/test/wiredtests/test-readstorm.ts b/test/wiredtests/test-readstorm.ts index 559c8ba..49214df 100644 --- a/test/wiredtests/test-readstorm.ts +++ b/test/wiredtests/test-readstorm.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint, LogLevel } from "../../src"; +import { Connection, Datapoint } from "../../src"; import test from "tape"; import util from "util"; import options from "./wiredtest-options.js"; @@ -53,7 +53,7 @@ if (process.env.hasOwnProperty("WIREDTEST")) { } } var connection = new Connection({ - loglevel: LogLevel.Warn, + loglevel: 'warn', //forceTunneling: true, // minimumDelay: 100, handlers: { From 4663803ffa5e4e224c7ad532f42cc2244b17c0b7 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 14:33:02 +0200 Subject: [PATCH 06/36] fix: working on tests --- src/FSM.ts | 204 ++++++++++++++++++++--------------------- src/types/machina.d.ts | 11 ++- 2 files changed, 109 insertions(+), 106 deletions(-) diff --git a/src/FSM.ts b/src/FSM.ts index 54455b1..31efc4b 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -14,7 +14,6 @@ import { LogLevel } from "log-driver"; type KnxDeviceAddress = string; - type KnxGroupAddress = string; /** The type of the KnxValue depends on the DPT that it is associated with */ @@ -115,104 +114,10 @@ export interface Datagram { }; } -export class KnxFSM extends machina.Fsm { - private options: KnxOptions; - private log: any; - private ThreeLevelGroupAddressing: boolean; - private reconnection_cycles: number; - private sentTunnRequests: { [key: string]: Datagram }; - private useTunneling: boolean; - private remoteEndpoint: { - addrstring: string; - addr: any; - port: number; - }; - private localEchoInTunneling: boolean | undefined; - private channel_id?: any; - private conntime?: number; - private lastSentTime?: number; - private connecttimer?: NodeJS.Timeout; - private disconnecttimer?: NodeJS.Timeout; - private connstatetimer?: NodeJS.Timeout; - private idletimer?: NodeJS.Timeout; - private tunnelingAckTimer?: NodeJS.Timeout; - private seqnum: number; - private seqnumRecv: number; - - private writer: Writer; - private socket: Socket; - - public localAddress: string | null; - - constructor(options: KnxOptions) { - super(); - - this.options = options || {}; - - if (typeof options.handlers === "object") { - for (const [key, value] of Object.entries(options.handlers)) { - if (typeof value === "function") { - this.on(key, value); - } - } - } - // boot up the KNX connection unless told otherwise - if (!options.manualConnect) this.Connect(); - } - - initialize(options: KnxOptions) { - this.options = options || {}; - this.log = KnxLog.get(options); - this.localAddress = null; - this.ThreeLevelGroupAddressing = true; - this.reconnection_cycles = 0; - this.sentTunnRequests = {}; - this.useTunneling = options.forceTunneling || false; - this.remoteEndpoint = { - addrstring: options.ipAddr || "224.0.23.12", - addr: ipaddr.parse(options.ipAddr || "224.0.23.12"), - port: options.ipPort || 3671, - }; - const range = this.remoteEndpoint.addr.range(); - this.localEchoInTunneling = - typeof options.localEchoInTunneling !== "undefined" - ? options.localEchoInTunneling - : false; - this.log.debug( - "initializing %s connection to %s", - range, - this.remoteEndpoint.addrstring - ); - switch (range) { - case "multicast": - if (this.localEchoInTunneling) { - this.localEchoInTunneling = false; - this.log.debug( - "localEchoInTunneling: true but DISABLED because i am on multicast" - ); - } - IpRoutingConnection(this); - break; - case "unicast": - case "private": - case "loopback": - this.useTunneling = true; - IpTunnelingConnection(this); - break; - default: - throw util.format( - "IP address % (%s) cannot be used for KNX", - options.ipAddr, - range - ); - } - } - - namespace: string = "knxnet"; - - initialState: string = "uninitialized"; - - states = { +const KnxFSM = machina.Fsm.extend({ + namespace: "knxnet", + initialState: "uninitialized", + states: { uninitialized: { ["*"]() { this.transition("connecting"); @@ -599,7 +504,103 @@ export class KnxFSM extends machina.Fsm { this.deferUntilTransition("idle"); }, }, + }, +}); + +export class KnxFSMConnection extends KnxFSM { + private options: KnxOptions; + private log: any; + private ThreeLevelGroupAddressing: boolean; + private reconnection_cycles: number; + private sentTunnRequests: { [key: string]: Datagram }; + private useTunneling: boolean; + private remoteEndpoint: { + addrstring: string; + addr: any; + port: number; }; + private localEchoInTunneling: boolean | undefined; + private channel_id?: any; + private conntime?: number; + private lastSentTime?: number; + private connecttimer?: NodeJS.Timeout; + private disconnecttimer?: NodeJS.Timeout; + private connstatetimer?: NodeJS.Timeout; + private idletimer?: NodeJS.Timeout; + private tunnelingAckTimer?: NodeJS.Timeout; + private seqnum: number; + private seqnumRecv: number; + + private writer: Writer; + private socket: Socket; + + public localAddress: string | null; + + constructor(options: KnxOptions) { + super(); + + this.options = options || {}; + this.log = KnxLog.get(options); + this.localAddress = null; + this.ThreeLevelGroupAddressing = true; + this.reconnection_cycles = 0; + this.sentTunnRequests = {}; + this.useTunneling = options.forceTunneling || false; + this.remoteEndpoint = { + addrstring: options.ipAddr || "224.0.23.12", + addr: ipaddr.parse(options.ipAddr || "224.0.23.12"), + port: options.ipPort || 3671, + }; + const range = this.remoteEndpoint.addr.range(); + this.localEchoInTunneling = + typeof options.localEchoInTunneling !== "undefined" + ? options.localEchoInTunneling + : false; + this.log.debug( + "initializing %s connection to %s", + range, + this.remoteEndpoint.addrstring + ); + switch (range) { + case "multicast": + if (this.localEchoInTunneling) { + this.localEchoInTunneling = false; + this.log.debug( + "localEchoInTunneling: true but DISABLED because i am on multicast" + ); + } + IpRoutingConnection(this); + break; + case "unicast": + case "private": + case "loopback": + this.useTunneling = true; + IpTunnelingConnection(this); + break; + default: + throw util.format( + "IP address % (%s) cannot be used for KNX", + options.ipAddr, + range + ); + } + + if (typeof options.handlers === "object") { + for (const [key, value] of Object.entries(options.handlers)) { + if (typeof value === "function") { + this.on(key, value); + } + } + } + // boot up the KNX connection unless told otherwise + if (!options.manualConnect) this.Connect(); + } + + /** + * -------------------------------- + * KNX FSM Utils methods + * -------------------------------- + */ acknowledge(datagram: Datagram) { const ack = this.prepareDatagram(KnxConstants.SERVICE_TYPE.TUNNELING_ACK); @@ -680,7 +681,7 @@ export class KnxFSM extends machina.Fsm { /** * -------------------------------- - * CONNECTION MANAGEMENT + * Connection management methods * -------------------------------- */ @@ -1002,7 +1003,6 @@ export class KnxFSM extends machina.Fsm { } Disconnect(cb: () => void): void { - if (this.state === "connecting") { KnxLog.get().debug("Disconnecting directly"); this.transition("uninitialized"); @@ -1076,6 +1076,4 @@ const AddTunn = (datagram: Datagram): void => { }; }; -const KnxFSMConnection = machina.Fsm.extend(KnxFSM) - export default KnxFSMConnection; diff --git a/src/types/machina.d.ts b/src/types/machina.d.ts index 2f34831..1d035ef 100644 --- a/src/types/machina.d.ts +++ b/src/types/machina.d.ts @@ -31,17 +31,22 @@ declare module "machina" { states: any; // Record; state: string; - static extend( - protoProps: T, + static extend( + protoProps: Partial, staticProps?: any - ): T & BehavioralFsm; + ): typeof BehavioralFsm; compositeState(client: BehavioralFsm): any; clearQueue(client: BehavioralFsm, name?: string): void; handle(client: BehavioralFsm, ...args: any[]): any; transition(client: BehavioralFsm, newState: string): void; + initialize(...args: any): void; } export class Fsm extends BehavioralFsm { + static extend( + protoProps: Partial, + staticProps?: any + ): typeof Fsm; compositeState(): any; clearQueue(name?: string): void; handle(...args: any[]): any; From 914cab44167e647a2739151b54c16440f8969b73 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 15:02:05 +0200 Subject: [PATCH 07/36] fix: make tests work --- package-lock.json | 1463 +++++++++++++---- package.json | 16 +- src/IpRoutingConnection.ts | 6 +- src/IpTunnelingConnection.ts | 6 +- src/dptlib/dpt1.ts | 2 +- src/dptlib/dpt10.ts | 2 +- src/dptlib/dpt11.ts | 2 +- src/dptlib/dpt12.ts | 2 +- src/dptlib/dpt13.ts | 2 +- src/dptlib/dpt14.ts | 2 +- src/dptlib/dpt15.ts | 2 +- src/dptlib/dpt16.ts | 2 +- src/dptlib/dpt17.ts | 2 +- src/dptlib/dpt18.ts | 2 +- src/dptlib/dpt19.ts | 2 +- src/dptlib/dpt2.ts | 2 +- src/dptlib/dpt20.ts | 2 +- src/dptlib/dpt21.ts | 2 +- src/dptlib/dpt232.ts | 2 +- src/dptlib/dpt237.ts | 2 +- src/dptlib/dpt238.ts | 2 +- src/dptlib/dpt3.ts | 2 +- src/dptlib/dpt4.ts | 2 +- src/dptlib/dpt5.ts | 2 +- src/dptlib/dpt6.ts | 2 +- src/dptlib/dpt7.ts | 2 +- src/dptlib/dpt8.ts | 2 +- src/dptlib/dpt9.ts | 2 +- src/dptlib/index.ts | 3 +- .../wiredtests/test-connect-routing-hybrid.ts | 58 +- test/wiredtests/test-connect-tunnel.ts | 80 +- test/wiredtests/test-control.ts | 4 - test/wiredtests/test-logotimer.ts | 78 +- test/wiredtests/test-read.ts | 81 +- test/wiredtests/test-readstorm.ts | 142 +- 35 files changed, 1380 insertions(+), 605 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84a526e..0240bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,15 @@ "dependencies": { "binary-parser": "^2.2.1", "binary-protocol": "0.0.0", - "ipaddr.js": "1.2.0", + "ipaddr.js": "2.1.0", "log-driver": "1.2.7", "machina": "^4.0.2" }, "devDependencies": { - "@types/node": "^20.12.2", + "@types/node": "^20.12.3", "@types/tape": "^5.6.4", "esbuild-register": "^3.5.0", - "multi-tape": "^1.2.1", - "tape": "^4.10.1", + "tape": "^5.7.5", "typescript": "^5.4.3" }, "engines": { @@ -418,10 +417,35 @@ "node": ">=12" } }, + "node_modules/@ljharb/resumer": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@ljharb/resumer/-/resumer-0.1.3.tgz", + "integrity": "sha512-d+tsDgfkj9X5QTriqM4lKesCkMMJC3IrbPKHvayP00ELx2axdXvDfWkqjxrLXIzGcQzmj7VAUT1wopqARTvafw==", + "dev": true, + "dependencies": { + "@ljharb/through": "^2.3.13", + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/@types/node": { - "version": "20.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz", - "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "version": "20.12.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.3.tgz", + "integrity": "sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -446,6 +470,78 @@ "@types/node": "*" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.every": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/array.prototype.every/-/array.prototype.every-1.1.6.tgz", + "integrity": "sha512-gNEqZD97w6bfQRNmHkFv7rNnGM+VWyHZT+h/rf9C+22owcXuENr66Lfo0phItpU5KoXW6Owb34q2+8MnSIZ57w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -484,13 +580,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -502,6 +604,57 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -520,39 +673,79 @@ } }, "node_modules/deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { - "object-keys": "^1.0.12" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/dotignore": { "version": "0.1.2", @@ -567,23 +760,57 @@ } }, "node_modules/es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", "dev": true, "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -592,28 +819,71 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-abstract/node_modules/is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "dev": true, "dependencies": { - "has-symbols": "^1.0.1" + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-abstract/node_modules/object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { @@ -684,12 +954,6 @@ "esbuild": ">=0.12 <1" } }, - "node_modules/events-to-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", - "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", - "dev": true - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -702,39 +966,100 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -745,23 +1070,14 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has": { + "node_modules/globalthis": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", "dev": true, "dependencies": { - "function-bind": "^1.1.1" + "define-properties": "^1.1.3" }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -769,55 +1085,59 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ipaddr.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.2.0.tgz", - "integrity": "sha1-irpJyRknmVhb3WQ+DMtQ6K53e6Q=", - "engines": { - "node": ">= 0.10" + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "node_modules/has-dynamic-import": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-dynamic-import/-/has-dynamic-import-2.1.0.tgz", + "integrity": "sha512-su0anMkNEnJKZ/rB99jn3y6lV/J8Ro96hBJ28YAeVzj5rWxH+YL/AdCyiYYA1HDLV9YhmvqpWSJJj2KLo1MX6g==", "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "engines": { - "node": ">= 0.4" + "dependencies": { + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -826,22 +1146,25 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", - "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -850,28 +1173,350 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "dependencies": { - "has-symbols": "^1.0.1" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "node_modules/log-driver": { - "version": "1.2.7", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "node_modules/log-driver": { + "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", "engines": { @@ -890,9 +1535,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -902,19 +1547,32 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "node_modules/mock-property": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mock-property/-/mock-property-1.0.3.tgz", + "integrity": "sha512-2emPTb1reeLLYwHxyVx993iYyCHEiRRO+y8NFXFPL5kl5q14sgTK76cXyEKkeKCHeRw35SfdkUJ10Q1KfHuiIQ==", "dev": true, "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "define-data-property": "^1.1.1", + "functions-have-names": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "hasown": "^2.0.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/ms": { @@ -923,39 +1581,23 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/multi-tape": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/multi-tape/-/multi-tape-1.4.0.tgz", - "integrity": "sha512-uPnzBRNyJdWgC4N8ta2r0gwQMgRY69v0M2JNAwTu3uzfdjYCeLQXDAfjYpcR1agZo+czuCQNbeCTi0+IVswklA==", - "dev": true, - "dependencies": { - "glob": "^7.1.4", - "minimist": "^1.2.0", - "stream-buffers": "^3.0.2", - "tap-parser": "^9.3.2", - "tee": "^0.2.0" - }, - "bin": { - "multi-tape": "build/index.js" - } - }, "node_modules/object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-is": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", - "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -974,14 +1616,14 @@ } }, "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, "engines": { @@ -994,7 +1636,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { "wrappy": "1" @@ -1003,26 +1645,37 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -1031,119 +1684,130 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexp.prototype.flags/node_modules/es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "dependencies": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, - "engines": { - "node": ">= 0.4" + "bin": { + "resolve": "bin/resolve" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexp.prototype.flags/node_modules/is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "has-symbols": "^1.0.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" }, "engines": { - "node": ">= 0.4" + "node": ">=0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexp.prototype.flags/node_modules/object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "path-parse": "^1.0.6" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, - "node_modules/resumer": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", - "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "through": "~2.3.4" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/resumer/node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/stream-buffers": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", - "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, "engines": { - "node": ">= 0.10.0" + "node": ">= 0.4" } }, "node_modules/string.prototype.trim": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz", - "integrity": "sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -1153,100 +1817,156 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", - "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", - "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tap-parser": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-9.3.3.tgz", - "integrity": "sha512-VlC7tlSZ3EGt2qPLSa9CTJepNkc2yCh7uzhzAF5DxnuujeKbFbKxMA+fxtTWEN2j/KgfGi+mgooiZPKkOhEQyw==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tape": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/tape/-/tape-5.7.5.tgz", + "integrity": "sha512-C5Gm1MR8ujZmNrsmOiHSkKFfY2thrnUrFw/fFtcva9FABbN7LrHuQPi3MTS0Z0i/SLfYSJtRIcJYDUpwPsQ8yA==", "dev": true, "dependencies": { - "events-to-array": "^1.0.1", - "minipass": "^2.2.0", - "tap-yaml": "^1.0.0" + "@ljharb/resumer": "^0.1.2", + "@ljharb/through": "^2.3.12", + "array.prototype.every": "^1.1.5", + "call-bind": "^1.0.7", + "deep-equal": "^2.2.3", + "defined": "^1.0.1", + "dotignore": "^0.1.2", + "for-each": "^0.3.3", + "get-package-type": "^0.1.0", + "glob": "^7.2.3", + "has-dynamic-import": "^2.1.0", + "hasown": "^2.0.1", + "inherits": "^2.0.4", + "is-regex": "^1.1.4", + "minimist": "^1.2.8", + "mock-property": "^1.0.3", + "object-inspect": "^1.13.1", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "resolve": "^2.0.0-next.5", + "string.prototype.trim": "^1.2.8" }, "bin": { - "tap-parser": "bin/cmd.js" + "tape": "bin/tape" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tap-yaml": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-1.0.0.tgz", - "integrity": "sha512-Rxbx4EnrWkYk0/ztcm5u3/VznbyFJpyXO12dDBHKWiDVxy7O2Qw6MRrwO5H6Ww0U5YhRY/4C/VzWmFPhBQc4qQ==", + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "yaml": "^1.5.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/tape": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/tape/-/tape-4.13.3.tgz", - "integrity": "sha512-0/Y20PwRIUkQcTCSi4AASs+OANZZwqPKaipGCEwp10dQMipVvSZwUUCi01Y/OklIGyHKFhIcjock+DKnBfLAFw==", - "dev": true, - "dependencies": { - "deep-equal": "~1.1.1", - "defined": "~1.0.0", - "dotignore": "~0.1.2", - "for-each": "~0.3.3", - "function-bind": "~1.1.1", - "glob": "~7.1.6", - "has": "~1.0.3", - "inherits": "~2.0.4", - "is-regex": "~1.0.5", - "minimist": "~1.2.5", - "object-inspect": "~1.7.0", - "resolve": "~1.17.0", - "resumer": "~0.0.0", - "string.prototype.trim": "~1.2.1", - "through": "~2.3.8" + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, - "bin": { - "tape": "bin/tape" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tape/node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/tee": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tee/-/tee-0.2.0.tgz", - "integrity": "sha1-2HtYIHoMmb1icXnCwsYjXj051yU=", + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "through": "~1.1.1" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/through": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/through/-/through-1.1.2.tgz", - "integrity": "sha1-NEpUJaN3MxTKfg62US+6+vdsC/4=", - "dev": true + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/typescript": { "version": "5.4.3", @@ -1261,32 +1981,85 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/wrappy": { + "node_modules/which-boxed-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/yaml": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true } } } diff --git a/package.json b/package.json index a51acb7..9f43c5b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "KNXnet/IP protocol implementation for Node(>=6.x)", "version": "2.5.4", "engines": { - "node": ">=6" + "node": ">=16" }, "private": false, "repository": { @@ -12,7 +12,12 @@ }, "scripts": { "build": "tsc -p tsconfig.build.json", - "test": "node -r esbuild-register ./node_modules/.bin/multi-tape test/*/test-*.ts" + "test": "npm run test:conn && npm run test:dptlib && npm run test:proto", + "test:tape": "node -r esbuild-register ./node_modules/.bin/tape", + "test:conn": "npm run test:tape -- test/connection/*.ts", + "test:dptlib": "npm run test:tape -- test/dptlib/*.ts", + "test:proto": "npm run test:tape -- test/knxproto/*.ts", + "test:wired": "npm run test:tape -- test/wiredtests/*.ts" }, "main": "./build/index.js", "license": "MIT", @@ -92,16 +97,15 @@ "dependencies": { "binary-parser": "^2.2.1", "binary-protocol": "0.0.0", - "ipaddr.js": "1.2.0", + "ipaddr.js": "2.1.0", "log-driver": "1.2.7", "machina": "^4.0.2" }, "devDependencies": { - "@types/node": "^20.12.2", + "@types/node": "^20.12.3", "@types/tape": "^5.6.4", "esbuild-register": "^3.5.0", - "multi-tape": "^1.2.1", - "tape": "^4.10.1", + "tape": "^5.7.5", "typescript": "^5.4.3" } } diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index dede8bf..298eb70 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -1,9 +1,9 @@ import * as util from 'util'; import * as dgram from 'dgram'; import KnxLog from './KnxLog'; -import { KnxFSM } from './FSM'; +import KnxFSMConnection from './FSM'; -function IpRoutingConnection(instance: KnxFSM): KnxFSM { +function IpRoutingConnection(instance: KnxFSMConnection): KnxFSMConnection { const log = KnxLog.get(); instance.BindSocket = function (cb: (socket: dgram.Socket) => void): dgram.Socket { @@ -34,7 +34,7 @@ function IpRoutingConnection(instance: KnxFSM): KnxFSM { // /// Start the connection /// - instance.Connect = function (): KnxFSM { + instance.Connect = function (): KnxFSMConnection { this.localAddress = this.getLocalAddress(); this.socket = this.BindSocket((socket: dgram.Socket) => { socket.on('error', (errmsg: string) => diff --git a/src/IpTunnelingConnection.ts b/src/IpTunnelingConnection.ts index c957425..45e2801 100644 --- a/src/IpTunnelingConnection.ts +++ b/src/IpTunnelingConnection.ts @@ -1,8 +1,8 @@ import dgram from "dgram"; import KnxLog from "./KnxLog"; -import { KnxFSM } from "./FSM"; +import KnxFSMConnection from "./FSM"; -function IpTunnelingConnection(instance: KnxFSM): KnxFSM { +function IpTunnelingConnection(instance: KnxFSMConnection): KnxFSMConnection { const log = KnxLog.get(); instance.BindSocket = function ( @@ -20,7 +20,7 @@ function IpTunnelingConnection(instance: KnxFSM): KnxFSM { return udpSocket; }; - instance.Connect = function (): KnxFSM { + instance.Connect = function (): KnxFSMConnection { this.localAddress = this.getLocalAddress(); // create the socket this.socket = this.BindSocket((socket: dgram.Socket) => { diff --git a/src/dptlib/dpt1.ts b/src/dptlib/dpt1.ts index 42e2e7c..448a4ad 100644 --- a/src/dptlib/dpt1.ts +++ b/src/dptlib/dpt1.ts @@ -18,7 +18,7 @@ const custom_truthiness = (value: string | boolean): boolean => { }; const config: DatapointConfig = { - id: "dpt1", + id: "DPT1", formatAPDU: (value) => Buffer.from([custom_truthiness(value) ? 1 : 0]), fromBuffer: (buf) => { diff --git a/src/dptlib/dpt10.ts b/src/dptlib/dpt10.ts index d0fb973..1ca0c96 100644 --- a/src/dptlib/dpt10.ts +++ b/src/dptlib/dpt10.ts @@ -17,7 +17,7 @@ const dowTimeRegexp = /((\d)\/)?(\d{1,2}):(\d{1,2}):(\d{1,2})/; // Always 8-bit aligned. const config: DatapointConfig = { - id: "dpt10", + id: "DPT10", formatAPDU: (value) => { let dow: number, hour: number, minute: number, second: number; // day of week. NOTE: JS Sunday = 0 diff --git a/src/dptlib/dpt11.ts b/src/dptlib/dpt11.ts index 90271d9..20bb538 100644 --- a/src/dptlib/dpt11.ts +++ b/src/dptlib/dpt11.ts @@ -11,7 +11,7 @@ const log = logger.get(); // DPT11.*: date // const config: DatapointConfig = { - id: "dpt11", + id: "DPT11", formatAPDU: (value) => { if (value == null) return log.error("cannot write null value for DPT11"); switch (typeof value) { diff --git a/src/dptlib/dpt12.ts b/src/dptlib/dpt12.ts index 9a2ba77..011f92b 100644 --- a/src/dptlib/dpt12.ts +++ b/src/dptlib/dpt12.ts @@ -11,7 +11,7 @@ import { DatapointConfig } from "."; // DPT12 base type info const config: DatapointConfig = { - id: "dpt12", + id: "DPT12", basetype: { bitlength: 32, signedness: "unsigned", diff --git a/src/dptlib/dpt13.ts b/src/dptlib/dpt13.ts index 76e7e68..c2b55c7 100644 --- a/src/dptlib/dpt13.ts +++ b/src/dptlib/dpt13.ts @@ -11,7 +11,7 @@ import { DatapointConfig } from "."; // DPT13 base type info const config: DatapointConfig = { - id: "dpt13", + id: "DPT13", basetype: { bitlength: 32, signedness: "signed", diff --git a/src/dptlib/dpt14.ts b/src/dptlib/dpt14.ts index 5c24f9d..4ff8b16 100644 --- a/src/dptlib/dpt14.ts +++ b/src/dptlib/dpt14.ts @@ -15,7 +15,7 @@ const log = logger.get(); * the case for 32-bit floating point is simple... */ const config: DatapointConfig = { - id: "dpt14", + id: "DPT14", formatAPDU: (value) => { if (value == null || typeof value != "number") log.error("DPT14: Must supply a number value"); diff --git a/src/dptlib/dpt15.ts b/src/dptlib/dpt15.ts index dbbed94..ae557ac 100644 --- a/src/dptlib/dpt15.ts +++ b/src/dptlib/dpt15.ts @@ -14,7 +14,7 @@ import { DatapointConfig } from "."; // DPT15 base type info const config: DatapointConfig = { - id: "dpt15", + id: "DPT15", basetype: { bitlength: 32, valuetype: "basic", diff --git a/src/dptlib/dpt16.ts b/src/dptlib/dpt16.ts index 6218d61..f4b1c1d 100644 --- a/src/dptlib/dpt16.ts +++ b/src/dptlib/dpt16.ts @@ -13,7 +13,7 @@ const log = logger.get(); // const config: DatapointConfig = { - id: "dpt16", + id: "DPT16", formatAPDU: (value) => { if (typeof value !== "string") return log.warn("Must supply a string value"); diff --git a/src/dptlib/dpt17.ts b/src/dptlib/dpt17.ts index 693bd07..329cc14 100644 --- a/src/dptlib/dpt17.ts +++ b/src/dptlib/dpt17.ts @@ -12,7 +12,7 @@ import { DatapointConfig } from "."; // TODO: implement fromBuffer, formatAPDU const config: DatapointConfig = { - id: "dpt17", + id: "DPT17", // DPT17 basetype info basetype: { bitlength: 8, diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts index 2394863..d5b455d 100644 --- a/src/dptlib/dpt18.ts +++ b/src/dptlib/dpt18.ts @@ -28,7 +28,7 @@ import { DatapointConfig } from "."; const log = logger.get(); const config: DatapointConfig = { - id: "dpt18", + id: "DPT18", formatAPDU: function (value) { if (value == null) log.warn("DPT18: cannot write null value"); else { diff --git a/src/dptlib/dpt19.ts b/src/dptlib/dpt19.ts index bc85e53..5514cce 100644 --- a/src/dptlib/dpt19.ts +++ b/src/dptlib/dpt19.ts @@ -14,7 +14,7 @@ const log = logger.get(); // const config: DatapointConfig = { - id: "dpt19", + id: "DPT19", formatAPDU: (value) => { if (!(value instanceof Date)) return log.error("DPT19: Must supply a Date object"); diff --git a/src/dptlib/dpt2.ts b/src/dptlib/dpt2.ts index 4cd490e..307e690 100644 --- a/src/dptlib/dpt2.ts +++ b/src/dptlib/dpt2.ts @@ -14,7 +14,7 @@ interface Dpt2Value { } const config: DatapointConfig = { - id: "dpt2", + id: "DPT2", // DPT2 frame description. // Always 8-bit aligned. diff --git a/src/dptlib/dpt20.ts b/src/dptlib/dpt20.ts index faefc24..94a720d 100644 --- a/src/dptlib/dpt20.ts +++ b/src/dptlib/dpt20.ts @@ -14,7 +14,7 @@ const log = logger.get(); // FIXME: help needed const config: DatapointConfig = { - id: "dpt20", + id: "DPT20", formatAPDU: (value) => { log.debug("./knx/src/dpt20.js : input value = " + value); return Buffer.from([value]); diff --git a/src/dptlib/dpt21.ts b/src/dptlib/dpt21.ts index 8803043..62a8e3f 100644 --- a/src/dptlib/dpt21.ts +++ b/src/dptlib/dpt21.ts @@ -19,7 +19,7 @@ const log = logger.get(); // FIXME: help needed const config: DatapointConfig = { - id: "dpt21", + id: "DPT21", formatAPDU: function (value) { if (value == null) return log.error("DPT21: cannot write null value"); log.debug("./knx/src/dpt21.js : input value = " + value); diff --git a/src/dptlib/dpt232.ts b/src/dptlib/dpt232.ts index 7ed436e..8cae4a2 100644 --- a/src/dptlib/dpt232.ts +++ b/src/dptlib/dpt232.ts @@ -13,7 +13,7 @@ const log = logger.get(); // MSB: Red, Green, LSB: Blue // const config: DatapointConfig = { - id: "dpt232", + id: "DPT232", formatAPDU: (value) => { if (value == null) return log.error("DPT232: cannot write null value"); diff --git a/src/dptlib/dpt237.ts b/src/dptlib/dpt237.ts index c69fb3e..0dba4de 100644 --- a/src/dptlib/dpt237.ts +++ b/src/dptlib/dpt237.ts @@ -12,7 +12,7 @@ const log = logger.get(); // DPT237: 2-byte unsigned value // const config: DatapointConfig = { - id: "dpt237", + id: "DPT237", formatAPDU: function (value) { if (value == null) return log.error("DPT237: cannot write null value"); diff --git a/src/dptlib/dpt238.ts b/src/dptlib/dpt238.ts index 42107ce..ec32922 100644 --- a/src/dptlib/dpt238.ts +++ b/src/dptlib/dpt238.ts @@ -13,7 +13,7 @@ const log = logger.get(); // // DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) const config: DatapointConfig = { - id: "dpt238", + id: "DPT238", formatAPDU: (value) => { const apdu_data = Buffer.from([value]); log.trace( diff --git a/src/dptlib/dpt3.ts b/src/dptlib/dpt3.ts index 548e133..d041c99 100644 --- a/src/dptlib/dpt3.ts +++ b/src/dptlib/dpt3.ts @@ -14,7 +14,7 @@ interface Dpt3Value { } const config: DatapointConfig = { - id: "dpt3", + id: "DPT3", formatAPDU: (value: Dpt3Value) => { if (value == null) return log.warn("DPT3: cannot write null value"); diff --git a/src/dptlib/dpt4.ts b/src/dptlib/dpt4.ts index c1d6906..b781e60 100644 --- a/src/dptlib/dpt4.ts +++ b/src/dptlib/dpt4.ts @@ -12,7 +12,7 @@ const log = KnxLog.get(); // const config: DatapointConfig = { - id: "dpt4", + id: "DPT4", formatAPDU: (value: string): Buffer | void => { if (value == null) return log.warn("DPT4: cannot write null value"); diff --git a/src/dptlib/dpt5.ts b/src/dptlib/dpt5.ts index 17cf8d3..2c7f65d 100644 --- a/src/dptlib/dpt5.ts +++ b/src/dptlib/dpt5.ts @@ -11,7 +11,7 @@ import { DatapointConfig } from "."; // DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) const config: DatapointConfig = { - id: "dpt5", + id: "DPT5", basetype: { bitlength: 8, signedness: "unsigned", diff --git a/src/dptlib/dpt6.ts b/src/dptlib/dpt6.ts index 36c74d2..88f3398 100644 --- a/src/dptlib/dpt6.ts +++ b/src/dptlib/dpt6.ts @@ -11,7 +11,7 @@ import { DatapointConfig } from "."; // DPT Basetype info const config: DatapointConfig = { - id: "dpt6", + id: "DPT6", basetype: { bitlength: 8, signedness: "signed", diff --git a/src/dptlib/dpt7.ts b/src/dptlib/dpt7.ts index 7a23804..e679c7f 100644 --- a/src/dptlib/dpt7.ts +++ b/src/dptlib/dpt7.ts @@ -9,7 +9,7 @@ import { DatapointConfig } from "."; // DPT7: 16-bit unsigned integer value // const config: DatapointConfig = { - id: "dpt7", + id: "DPT7", basetype: { bitlength: 16, signedness: "unsigned", diff --git a/src/dptlib/dpt8.ts b/src/dptlib/dpt8.ts index 36585b7..a123274 100644 --- a/src/dptlib/dpt8.ts +++ b/src/dptlib/dpt8.ts @@ -10,7 +10,7 @@ import { DatapointConfig } from "."; // const config: DatapointConfig = { - id: "dpt8", + id: "DPT8", // DPT8 basetype info basetype: { bitlength: 16, diff --git a/src/dptlib/dpt9.ts b/src/dptlib/dpt9.ts index 89cc8d3..166824b 100644 --- a/src/dptlib/dpt9.ts +++ b/src/dptlib/dpt9.ts @@ -35,7 +35,7 @@ const frexp = (value) => { }; const config: DatapointConfig = { - id: "dpt9", + id: "DPT9", formatAPDU: (value) => { if (!isFinite(value)) return log.warn("DPT9: cannot write non-numeric or undefined value"); diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts index 547b411..d305b44 100644 --- a/src/dptlib/index.ts +++ b/src/dptlib/index.ts @@ -127,7 +127,8 @@ export function resolve(dptid: string | number) { .match(/^(?:DPT)?(\d+)(\.(\d+))?$/); if (m === null) throw "Invalid DPT format: " + dptid; - const dpt = dpts[util.format("DPT%s", m[1])]; + const dptkey = util.format("DPT%s", m[1]) + const dpt = dpts[dptkey]; if (!dpt) throw "Unsupported DPT: " + dptid; const cloned_dpt = cloneDpt(dpt); diff --git a/test/wiredtests/test-connect-routing-hybrid.ts b/test/wiredtests/test-connect-routing-hybrid.ts index ee92537..049bfed 100644 --- a/test/wiredtests/test-connect-routing-hybrid.ts +++ b/test/wiredtests/test-connect-routing-hybrid.ts @@ -1,42 +1,38 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ Error.stackTraceLimit = Infinity; -import { Connection } from '../../src'; -import test from 'tape'; +import { Connection } from "../../src"; +import test from "tape"; /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== - to run all tests: $ WIREDTEST=1 npm test - to run one test : $ WIREDTEST=1 node test/wiredtests/.js */ -if (process.env.hasOwnProperty('WIREDTEST')) { - // - test('KNX connect routing hybrid', function(t) { - var connection = new Connection({ - loglevel: 'debug', - forceTunneling: true, - handlers: { - connected: function() { - t.pass('connected in hybrid mode'); - t.end(); - process.exit(0); - }, - error: function(connstatus) { - t.fail('error connecting: '+connstatus); - t.end(); - process.exit(1); - } - } - }); +// +test("KNX connect routing hybrid", function (t) { + var connection = new Connection({ + loglevel: "debug", + forceTunneling: true, + handlers: { + connected: function () { + t.pass("connected in hybrid mode"); + t.end(); + process.exit(0); + }, + error: function (connstatus) { + t.fail("error connecting: " + connstatus); + t.end(); + process.exit(1); + }, + }, }); +}); - setTimeout(function() { - console.log('Exiting with timeout...'); - process.exit(2); - }, 1000); -} +setTimeout(function () { + console.log("Exiting with timeout..."); + process.exit(2); +}, 1000); diff --git a/test/wiredtests/test-connect-tunnel.ts b/test/wiredtests/test-connect-tunnel.ts index 29d3e9b..c197e1d 100644 --- a/test/wiredtests/test-connect-tunnel.ts +++ b/test/wiredtests/test-connect-tunnel.ts @@ -1,55 +1,51 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ Error.stackTraceLimit = Infinity; -import { Connection } from '../../src'; -import test from 'tape'; +import { Connection } from "../../src"; +import test from "tape"; -import options from './wiredtest-options'; +import options from "./wiredtest-options"; /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== - to run all tests: $ WIREDTEST=1 npm test - to run one test : $ WIREDTEST=1 node test/wiredtests/.js */ -if (process.env.hasOwnProperty('WIREDTEST')) { - // - test('KNX connect tunneling', function(t) { - var connection = new Connection({ - // set up your KNX IP router's IP address (not multicast!) - // for getting into tunnelling mode - ipAddr: options.ipAddr, - physAddr: options.physAddr, - debug: true, - handlers: { - connected: function() { - console.log('----------'); - console.log('Connected!'); - console.log('----------'); - t.pass('connected in TUNNELING mode'); - this.Disconnect(); - }, - disconnected: function() { - t.pass('disconnected in TUNNELING mode'); - t.end(); - process.exit(0); - }, - error: function(connstatus) { - t.fail('error connecting: '+connstatus); - t.end(); - process.exit(1); - } - } - }); +// +test("KNX connect tunneling", function (t) { + var connection = new Connection({ + // set up your KNX IP router's IP address (not multicast!) + // for getting into tunnelling mode + ipAddr: options.ipAddr, + physAddr: options.physAddr, + debug: true, + handlers: { + connected: function () { + console.log("----------"); + console.log("Connected!"); + console.log("----------"); + t.pass("connected in TUNNELING mode"); + this.Disconnect(); + }, + disconnected: function () { + t.pass("disconnected in TUNNELING mode"); + t.end(); + process.exit(0); + }, + error: function (connstatus) { + t.fail("error connecting: " + connstatus); + t.end(); + process.exit(1); + }, + }, }); +}); - setTimeout(function() { - console.log('Exiting with timeout...'); - process.exit(2); - }, 1000); -} +setTimeout(function () { + console.log("Exiting with timeout..."); + process.exit(2); +}, 1000); diff --git a/test/wiredtests/test-control.ts b/test/wiredtests/test-control.ts index b0fff20..a383982 100644 --- a/test/wiredtests/test-control.ts +++ b/test/wiredtests/test-control.ts @@ -12,10 +12,7 @@ import options from "./wiredtest-options"; ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== - - $ WIREDTEST=1 node test/wiredtests/.js */ -if (process.env.hasOwnProperty('WIREDTEST')) { test('KNX wired test - control a basic DPT1 binary switch', function(t) { var counter = 0; @@ -77,4 +74,3 @@ if (process.env.hasOwnProperty('WIREDTEST')) { console.log('Exiting with timeout ...'); process.exit(2); }, 2000); -} diff --git a/test/wiredtests/test-logotimer.ts b/test/wiredtests/test-logotimer.ts index 050ca83..c2e2571 100644 --- a/test/wiredtests/test-logotimer.ts +++ b/test/wiredtests/test-logotimer.ts @@ -1,42 +1,54 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ -import { Connection, Datapoint } from '../../src'; -import test from 'tape'; -import util from 'util'; -import options from './wiredtest-options'; + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ +import { Connection, Datapoint } from "../../src"; +import test from "tape"; +import util from "util"; +import options from "./wiredtest-options"; /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== - to run all tests: $ WIREDTEST=1 npm test - to run one test : $ WIREDTEST=1 node test/wiredtests/.js */ -if (process.env.hasOwnProperty('WIREDTEST')) { - test('KNX wired test - control a DPT9 timer', function(t) { - var connection = new Connection( { - //debug: true, - handlers: { - connected: () => { - var timer_control = new Datapoint({ga: options.dpt9_timer_control_ga, dpt: 'DPT9.001', autoread: true}, connection); - var timer_status = new Datapoint({ga: options.dpt9_timer_status_ga, dpt: 'DPT9.001', autoread: true}, connection); - timer_control.on('change', function(oldvalue, newvalue) { - t.pass(util.format("**** Timer control changed from: %j to: %j", oldvalue, newvalue)); - }); - timer_status.read(function(src, response) { - t.pass(util.format("**** Timer status response: %j", response)); - t.end(); - process.exit(0); - }); - timer_control.write(12); - } - } - }); +test("KNX wired test - control a DPT9 timer", function (t) { + var connection = new Connection({ + //debug: true, + handlers: { + connected: () => { + var timer_control = new Datapoint( + { + ga: options.dpt9_timer_control_ga, + dpt: "DPT9.001", + autoread: true, + }, + connection + ); + var timer_status = new Datapoint( + { ga: options.dpt9_timer_status_ga, dpt: "DPT9.001", autoread: true }, + connection + ); + timer_control.on("change", function (oldvalue, newvalue) { + t.pass( + util.format( + "**** Timer control changed from: %j to: %j", + oldvalue, + newvalue + ) + ); + }); + timer_status.read(function (src, response) { + t.pass(util.format("**** Timer status response: %j", response)); + t.end(); + process.exit(0); + }); + timer_control.write(12); + }, + }, }); +}); - setTimeout(function () { - process.exit(1); - }, 1000); -} +setTimeout(function () { + process.exit(1); +}, 1000); diff --git a/test/wiredtests/test-read.ts b/test/wiredtests/test-read.ts index b687cff..49999af 100644 --- a/test/wiredtests/test-read.ts +++ b/test/wiredtests/test-read.ts @@ -1,51 +1,52 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { Connection, Datapoint } from '../../src'; -import test from 'tape'; -import util from 'util'; -import options from './wiredtest-options'; +import { Connection, Datapoint } from "../../src"; +import test from "tape"; +import util from "util"; +import options from "./wiredtest-options"; /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== - to run all tests: $ WIREDTEST=1 npm test - to run one test : $ WIREDTEST=1 node test/wiredtests/.js */ -if (process.env.hasOwnProperty('WIREDTEST')) { - test('KNX wired test - read a temperature', function(t) { - var connection = new Connection({ - debug: true, - physAddr: options.physAddr, - handlers: { - connected: function() { - // just define a temperature GA that should respond to a a GroupValue_Read request - var temperature_in = new Datapoint({ +test("KNX wired test - read a temperature", function (t) { + var connection = new Connection({ + debug: true, + physAddr: options.physAddr, + handlers: { + connected: function () { + // just define a temperature GA that should respond to a a GroupValue_Read request + var temperature_in = new Datapoint( + { ga: options.dpt9_temperature_status_ga, - dpt: 'DPT9.001' - }, connection); - temperature_in.read(function(src, response) { - console.log("KNX response from %s: %j", src, response); - t.pass(util.format('read temperature: %s', response)); - t.end(); - process.exit(0); - }); - }, - error: function(connstatus) { - console.log("%s **** ERROR: %j", - new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), - connstatus); - process.exit(1); - } - } - }); + dpt: "DPT9.001", + }, + connection + ); + temperature_in.read(function (src, response) { + console.log("KNX response from %s: %j", src, response); + t.pass(util.format("read temperature: %s", response)); + t.end(); + process.exit(0); + }); + }, + error: function (connstatus) { + console.log( + "%s **** ERROR: %j", + new Date().toISOString().replace(/T/, " ").replace(/Z$/, ""), + connstatus + ); + process.exit(1); + }, + }, }); +}); - setTimeout(function() { - console.log('Exiting ...'); - process.exit(2); - }, 1500); -} +setTimeout(function () { + console.log("Exiting ..."); + process.exit(2); +}, 1500); diff --git a/test/wiredtests/test-readstorm.ts b/test/wiredtests/test-readstorm.ts index 49214df..74c190c 100644 --- a/test/wiredtests/test-readstorm.ts +++ b/test/wiredtests/test-readstorm.ts @@ -14,82 +14,78 @@ Error.stackTraceLimit = Infinity; ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== - to run all tests: $ WIREDTEST=1 npm test - to run one test : $ WIREDTEST=1 node test/wiredtests/.js */ -if (process.env.hasOwnProperty("WIREDTEST")) { - test("KNX wired test - read multiple statuses from a consecutive GA range", function (t) { - var readback = {}; - function setupDatapoint(groupadress: string, statusga: string) { - var dp = new Datapoint( - { - ga: groupadress, - status_ga: statusga, - dpt: "DPT1.001", - autoread: true, - }, - connection - ); - dp.on("change", (oldvalue, newvalue) => { - console.log("**** %s current value: %j", groupadress, newvalue); - }); - return dp; - } - function setupDatapoints() { - var ctrl_ga_arr = options.readstorm_control_ga_start.split("/"); - var stat_ga_arr = options.readstorm_status_ga_start.split("/"); - for (let i = 0; i < options.readstorm_range; i++) { - var ctrl_ga = [ - ctrl_ga_arr[0], - ctrl_ga_arr[1], - i + parseInt(ctrl_ga_arr[2]), - ].join("/"); - var stat_ga = [ - stat_ga_arr[0], - stat_ga_arr[1], - i + parseInt(stat_ga_arr[2]), - ].join("/"); - setupDatapoint(ctrl_ga, stat_ga); - } +test("KNX wired test - read multiple statuses from a consecutive GA range", function (t) { + var readback = {}; + function setupDatapoint(groupadress: string, statusga: string) { + var dp = new Datapoint( + { + ga: groupadress, + status_ga: statusga, + dpt: "DPT1.001", + autoread: true, + }, + connection + ); + dp.on("change", (oldvalue, newvalue) => { + console.log("**** %s current value: %j", groupadress, newvalue); + }); + return dp; + } + function setupDatapoints() { + var ctrl_ga_arr = options.readstorm_control_ga_start.split("/"); + var stat_ga_arr = options.readstorm_status_ga_start.split("/"); + for (let i = 0; i < options.readstorm_range; i++) { + var ctrl_ga = [ + ctrl_ga_arr[0], + ctrl_ga_arr[1], + i + parseInt(ctrl_ga_arr[2]), + ].join("/"); + var stat_ga = [ + stat_ga_arr[0], + stat_ga_arr[1], + i + parseInt(stat_ga_arr[2]), + ].join("/"); + setupDatapoint(ctrl_ga, stat_ga); } - var connection = new Connection({ - loglevel: 'warn', - //forceTunneling: true, - // minimumDelay: 100, - handlers: { - connected: function () { - setupDatapoints(); - }, - event: function (evt, src, dest, value) { - if (evt == "GroupValue_Response") { - readback[dest] = [src, value]; - // have we got responses from all the read requests for all datapoints? - if (Object.keys(readback).length == options.readstorm_range) { - t.pass( - util.format( - "readstorm: all %d datapoints accounted for", - options.readstorm_range - ) - ); - t.end(); - process.exit(0); - } + } + var connection = new Connection({ + loglevel: "warn", + //forceTunneling: true, + // minimumDelay: 100, + handlers: { + connected: function () { + setupDatapoints(); + }, + event: function (evt, src, dest, value) { + if (evt == "GroupValue_Response") { + readback[dest] = [src, value]; + // have we got responses from all the read requests for all datapoints? + if (Object.keys(readback).length == options.readstorm_range) { + t.pass( + util.format( + "readstorm: all %d datapoints accounted for", + options.readstorm_range + ) + ); + t.end(); + process.exit(0); } - }, - error: function (connstatus) { - console.log( - "%s **** ERROR: %j", - new Date().toISOString().replace(/T/, " ").replace(/Z$/, ""), - connstatus - ); - process.exit(1); - }, + } }, - }); + error: function (connstatus) { + console.log( + "%s **** ERROR: %j", + new Date().toISOString().replace(/T/, " ").replace(/Z$/, ""), + connstatus + ); + process.exit(1); + }, + }, }); +}); - setTimeout(function () { - console.log("Exiting with timeout..."); - process.exit(2); - }, 1500); -} +setTimeout(function () { + console.log("Exiting with timeout..."); + process.exit(2); +}, 1500); From b7c4cb89b243dd97d3f597602a3dc481bbf8a543 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 15:48:53 +0200 Subject: [PATCH 08/36] feat: eslint + prettier --- .eslintignore | 6 + .eslintrc.js | 50 + .github/workflows/ci.yml | 10 +- .prettierignore | 7 + .prettierrc.js | 7 + .vscode/settings.json | 9 + package-lock.json | 2512 +++++++++++++++-- package.json | 11 + src/Address.ts | 144 +- src/Datapoint.ts | 216 +- src/FSM.ts | 2189 +++++++------- src/IpRoutingConnection.ts | 117 +- src/IpTunnelingConnection.ts | 82 +- src/KnxConstants.ts | 185 +- src/KnxLog.ts | 49 +- src/KnxProtocol.ts | 1082 +++---- src/devices/BinarySwitch.ts | 115 +- src/devices/index.ts | 6 +- src/dptlib/dpt1.ts | 450 +-- src/dptlib/dpt10.ts | 178 +- src/dptlib/dpt11.ts | 141 +- src/dptlib/dpt12.ts | 36 +- src/dptlib/dpt13.ts | 134 +- src/dptlib/dpt14.ts | 314 +-- src/dptlib/dpt15.ts | 32 +- src/dptlib/dpt16.ts | 84 +- src/dptlib/dpt17.ts | 30 +- src/dptlib/dpt18.ts | 120 +- src/dptlib/dpt19.ts | 92 +- src/dptlib/dpt2.ts | 285 +- src/dptlib/dpt20.ts | 72 +- src/dptlib/dpt21.ts | 122 +- src/dptlib/dpt232.ts | 84 +- src/dptlib/dpt237.ts | 126 +- src/dptlib/dpt238.ts | 122 +- src/dptlib/dpt3.ts | 93 +- src/dptlib/dpt4.ts | 90 +- src/dptlib/dpt5.ts | 102 +- src/dptlib/dpt6.ts | 62 +- src/dptlib/dpt7.ts | 194 +- src/dptlib/dpt8.ts | 138 +- src/dptlib/dpt9.ts | 441 +-- src/dptlib/index.ts | 381 ++- src/index.ts | 34 +- src/types/binary-protocol.d.ts | 98 +- src/types/log-driver.d.ts | 32 +- src/types/machina.d.ts | 108 +- src/utils.ts | 4 + test/connection/test-connect-routing.ts | 60 +- test/dptlib/commontest.ts | 104 +- test/dptlib/test-dpt.ts | 110 +- test/dptlib/test-dpt1.ts | 10 +- test/dptlib/test-dpt10.ts | 98 +- test/dptlib/test-dpt11.ts | 72 +- test/dptlib/test-dpt12.ts | 24 +- test/dptlib/test-dpt13.ts | 26 +- test/dptlib/test-dpt19.ts | 96 +- test/dptlib/test-dpt2.ts | 18 +- test/dptlib/test-dpt21.ts | 111 +- test/dptlib/test-dpt237.ts | 129 +- test/dptlib/test-dpt3.ts | 24 +- test/dptlib/test-dpt4.ts | 14 +- test/dptlib/test-dpt5.ts | 36 +- test/dptlib/test-dpt6.ts | 18 +- test/dptlib/test-dpt7.ts | 18 +- test/dptlib/test-dpt8.ts | 20 +- test/dptlib/test-dpt9.ts | 28 +- test/knxproto/test-address.ts | 132 +- test/knxproto/test-proto.ts | 520 ++-- .../wiredtests/test-connect-routing-hybrid.ts | 48 +- test/wiredtests/test-connect-tunnel.ts | 70 +- test/wiredtests/test-control.ts | 155 +- test/wiredtests/test-logotimer.ts | 90 +- test/wiredtests/test-read.ts | 81 +- test/wiredtests/test-readstorm.ts | 159 +- test/wiredtests/wiredtest-options.ts | 48 +- 76 files changed, 7883 insertions(+), 5432 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 .vscode/settings.json create mode 100644 src/utils.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..88cc887 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +/typescript-sample/ +/manualtest/ +/build/ + + +*.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a26beca --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,50 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint/eslint-plugin'], + env: { + node: true, + }, + extends: [ + 'airbnb-base', + 'airbnb-typescript/base', + 'plugin:prettier/recommended', + 'plugin:@typescript-eslint/recommended' + ], + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + tsconfigRootDir: __dirname + }, + rules: { + 'global-require': 'off', + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-unused-vars': 'off', + 'no-underscore-dangle': 'off', + 'no-param-reassign': 'off', + 'no-restricted-syntax': 'off', + camelcase: 'off', + 'default-case': 'off', + 'consistent-return': 'off', + 'import/order': 'off', + 'max-classes-per-file': 'off', + 'no-plusplus': 'off', + 'guard-for-in': 'off', + 'no-bitwise': 'off', + 'class-methods-use-this': 'off', + 'no-continue': 'off', + 'prefer-destructuring': 'off', + 'no-use-before-define': 'off', + 'no-restricted-globals': 'off', + 'radix': 'off', + 'func-names': 'off', + // Typescript rules + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/dot-notation': 'off', + '@typescript-eslint/no-use-before-define': 'off', + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4667ad4..500adbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,11 +25,11 @@ jobs: - name: Install Dependencies run: npm ci - # - name: Lint - # if: matrix.node-version == '20.x' - # # only run on latest node version, no reason to run on all - # run: | - # npm run lint + - name: Lint + if: matrix.node-version == '20.x' + # only run on latest node version, no reason to run on all + run: | + npm run lint - name: Test NodeJS run: npm run test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5e0281a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ + +/typescript-sample/ +/manualtest/ +/build/ + +*.md +README.md diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..3d8d73d --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + semi: false, + singleQuote: true, + useTabs: true, + tabWidth: 4, + endOfLine: "lf", +}; diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a50deb7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "eslint.format.enable": true, + "eslint.lintTask.enable": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.fixAll.markdownlint": "explicit" + }, +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a7873df..3be9d23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,16 @@ "devDependencies": { "@types/node": "^20.12.3", "@types/tape": "^5.6.4", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", "esbuild-register": "^3.5.0", + "eslint": "^8.57.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", "tape": "^5.7.5", "typescript": "^5.4.3" }, @@ -26,6 +35,15 @@ "node": ">=16" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", @@ -43,6 +61,95 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, "node_modules/@ljharb/resumer": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@ljharb/resumer/-/resumer-0.1.3.tgz", @@ -68,6 +175,65 @@ "node": ">= 0.4" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/node": { "version": "20.12.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.3.tgz", @@ -77,6 +243,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, "node_modules/@types/tape": { "version": "5.6.4", "resolved": "https://registry.npmjs.org/@types/tape/-/tape-5.6.4.tgz", @@ -96,183 +268,301 @@ "@types/node": "*" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz", + "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.5.0", + "@typescript-eslint/type-utils": "7.5.0", + "@typescript-eslint/utils": "7.5.0", + "@typescript-eslint/visitor-keys": "7.5.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/array.prototype.every": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/array.prototype.every/-/array.prototype.every-1.1.6.tgz", - "integrity": "sha512-gNEqZD97w6bfQRNmHkFv7rNnGM+VWyHZT+h/rf9C+22owcXuENr66Lfo0phItpU5KoXW6Owb34q2+8MnSIZ57w==", + "node_modules/@typescript-eslint/parser": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz", + "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0", - "is-string": "^1.0.7" + "@typescript-eslint/scope-manager": "7.5.0", + "@typescript-eslint/types": "7.5.0", + "@typescript-eslint/typescript-estree": "7.5.0", + "@typescript-eslint/visitor-keys": "7.5.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz", + "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "@typescript-eslint/types": "7.5.0", + "@typescript-eslint/visitor-keys": "7.5.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz", + "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==", "dev": true, "dependencies": { - "possible-typed-array-names": "^1.0.0" + "@typescript-eslint/typescript-estree": "7.5.0", + "@typescript-eslint/utils": "7.5.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "node_modules/@typescript-eslint/types": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz", + "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/binary-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", - "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz", + "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.5.0", + "@typescript-eslint/visitor-keys": "7.5.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/binary-protocol": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/binary-protocol/-/binary-protocol-0.0.0.tgz", - "integrity": "sha1-ZU4tbCB8HS7qpl1dais+ulmu024=", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { - "bluebird": "~2.2.2" + "balanced-match": "^1.0.0" } }, - "node_modules/bluebird": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.2.2.tgz", - "integrity": "sha1-8b8Fq8iHz5pwOIYjfChhCkOx8RQ=" + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@typescript-eslint/utils": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz", + "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.5.0", + "@typescript-eslint/types": "7.5.0", + "@typescript-eslint/typescript-estree": "7.5.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz", + "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==", "dev": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "@typescript-eslint/types": "7.5.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">= 0.4" + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -281,47 +571,46 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.every": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/array.prototype.every/-/array.prototype.every-1.1.6.tgz", + "integrity": "sha512-gNEqZD97w6bfQRNmHkFv7rNnGM+VWyHZT+h/rf9C+22owcXuENr66Lfo0phItpU5KoXW6Owb34q2+8MnSIZ57w==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0", + "is-string": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -330,15 +619,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, "dependencies": { - "es-define-property": "^1.0.0", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -347,15 +639,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -364,222 +657,1070 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dotignore": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", - "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "minimatch": "^3.0.4" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" }, - "bin": { - "ignored": "bin/ignored" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/binary-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", + "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/binary-protocol": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/binary-protocol/-/binary-protocol-0.0.0.tgz", + "integrity": "sha1-ZU4tbCB8HS7qpl1dais+ulmu024=", + "dependencies": { + "bluebird": "~2.2.2" + } + }, + "node_modules/bluebird": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.2.2.tgz", + "integrity": "sha1-8b8Fq8iHz5pwOIYjfChhCkOx8RQ=" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", + "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4" + }, + "bin": { + "ignored": "bin/ignored" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/esbuild-register": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", + "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-airbnb-typescript": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-18.0.0.tgz", + "integrity": "sha512-oc+Lxzgzsu8FQyFVa4QFaVKiitTYiiW3frB9KYW5OWdPrqFc7FzxgB20hP4cHMlr+MBzGcLl3jnCOVOydL9mIg==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.4" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": ">= 0.4" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" + "estraverse": "^5.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4.0" } }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { - "es-errors": "^1.3.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": ">= 0.4" + "node": ">=8.6.0" } }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "is-glob": "^4.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 6" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" }, "engines": { - "node": ">= 0.4" + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "hasInstallScript": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esbuild-register": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", - "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "debug": "^4.3.4" + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, - "peerDependencies": { - "esbuild": ">=0.12 <1" + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -696,6 +1837,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -711,6 +1879,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -723,6 +1911,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -748,6 +1942,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -811,6 +2014,40 @@ "node": ">= 0.4" } }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -963,6 +2200,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -987,6 +2245,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -1002,6 +2269,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -1136,11 +2412,102 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -1149,6 +2516,18 @@ "node": ">=0.8.6" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/machina": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/machina/-/machina-4.0.2.tgz", @@ -1160,6 +2539,28 @@ "node": ">=0.4.0" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1207,6 +2608,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -1259,6 +2666,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1268,6 +2738,74 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1277,12 +2815,42 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -1292,6 +2860,71 @@ "node": ">= 0.4" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -1327,6 +2960,63 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -1362,6 +3052,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1394,6 +3099,27 @@ "node": ">= 0.4" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -1412,6 +3138,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -1473,6 +3208,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -1485,6 +3265,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tape": { "version": "5.7.5", "resolved": "https://registry.npmjs.org/tape/-/tape-5.7.5.tgz", @@ -1521,6 +3317,78 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -1628,6 +3496,30 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -1686,6 +3578,24 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 9f43c5b..a4a1e4c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ }, "scripts": { "build": "tsc -p tsconfig.build.json", + "lint": "eslint --ext .ts .", + "lint-fix": "eslint --fix --ext .ts .", "test": "npm run test:conn && npm run test:dptlib && npm run test:proto", "test:tape": "node -r esbuild-register ./node_modules/.bin/tape", "test:conn": "npm run test:tape -- test/connection/*.ts", @@ -104,7 +106,16 @@ "devDependencies": { "@types/node": "^20.12.3", "@types/tape": "^5.6.4", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", "esbuild-register": "^3.5.0", + "eslint": "^8.57.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", "tape": "^5.7.5", "typescript": "^5.4.3" } diff --git a/src/Address.ts b/src/Address.ts index 3c23787..4cca8bf 100644 --- a/src/Address.ts +++ b/src/Address.ts @@ -2,12 +2,12 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import KnxLog from './KnxLog'; -import { Parser } from 'binary-parser'; +import KnxLog from './KnxLog' +import { Parser } from 'binary-parser' interface AddressType { - PHYSICAL: number; - GROUP: number; + PHYSICAL: number + GROUP: number } // +-----------------------------------------------+ // 16 bits | INDIVIDUAL ADDRESS | @@ -42,78 +42,82 @@ interface AddressType { // NOTE: ets4 can utilise all 5 bits for the main group (0..31) export const TYPE: AddressType = { - PHYSICAL: 0x00, - GROUP: 0x01, -}; + PHYSICAL: 0x00, + GROUP: 0x01, +} -const threeLevelPhysical = new Parser().bit4('l1').bit4('l2').uint8('l3'); -const threeLevelGroup = new Parser().bit5('l1').bit3('l2').uint8('l3'); -const twoLevel = new Parser().bit5('l1').bit11('l2'); +const threeLevelPhysical = new Parser().bit4('l1').bit4('l2').uint8('l3') +const threeLevelGroup = new Parser().bit5('l1').bit3('l2').uint8('l3') +const twoLevel = new Parser().bit5('l1').bit11('l2') // convert address stored in two-byte buffer to string -export function toString ( - buf: string | Buffer, - addrtype: number, - twoLevelAddressing = false +export function toString( + buf: string | Buffer, + addrtype: number, + twoLevelAddressing = false, ): string { - const group = addrtype == TYPE.GROUP; - //KnxLog.get().trace('%j, type: %d, %j', buf, addrtype, knxnetprotocol.twoLevelAddressing); - if (!Buffer.isBuffer(buf) || buf.length !== 2) - throw 'not a buffer, or not a 2-byte address buffer'; - if (group && twoLevelAddressing) { - // 2 level group - const { l1, l2 } = twoLevel.parse(buf); - return [l1, l2].join('/'); - } - // 3 level physical or group address - const sep = group ? '/' : '.'; - const parser = group ? threeLevelGroup : threeLevelPhysical; - const { l1, l2, l3 } = parser.parse(buf); - return [l1, l2, l3].join(sep); -}; + const group = addrtype === TYPE.GROUP + // KnxLog.get().trace('%j, type: %d, %j', buf, addrtype, knxnetprotocol.twoLevelAddressing); + if (!Buffer.isBuffer(buf) || buf.length !== 2) + throw Error('not a buffer, or not a 2-byte address buffer') + if (group && twoLevelAddressing) { + // 2 level group + const { l1, l2 } = twoLevel.parse(buf) + return [l1, l2].join('/') + } + // 3 level physical or group address + const sep = group ? '/' : '.' + const parser = group ? threeLevelGroup : threeLevelPhysical + const { l1, l2, l3 } = parser.parse(buf) + return [l1, l2, l3].join(sep) +} // check for out of range integer -const r = (x: number, max: number): boolean => x < 0 || x > max; +const r = (x: number, max: number): boolean => x < 0 || x > max // parse address string to 2-byte Buffer export function parse( - addr: string, - addrtype?: number, - twoLevelAddressing = false + addr: string, + addrtype?: number, + twoLevelAddressing = false, ): Buffer { - if (!addr) { - KnxLog.get().warn('Fix your code - no address given to Address.parse'); - } - const group = addrtype === TYPE.GROUP; - const address = Buffer.allocUnsafe(2); - const tokens = addr - .split(group ? '/' : '.') - .filter((w) => w.length > 0) - .map((w) => parseInt(w)); - if (tokens.length < 2) throw 'Invalid address (less than 2 tokens)'; - const [hinibble, midnibble, lonibble] = tokens; - if (group && twoLevelAddressing) { - // 2 level group address - if (r(hinibble, 31)) throw 'Invalid KNX 2-level main group: ' + addr; - if (r(midnibble, 2047)) throw 'Invalid KNX 2-level sub group: ' + addr; - address.writeUInt16BE((hinibble << 11) + midnibble, 0); - return address; - } - if (tokens.length < 3) throw 'Invalid address - missing 3rd token'; - if (group) { - // 3 level group address - if (r(hinibble, 31)) throw 'Invalid KNX 3-level main group: ' + addr; - if (r(midnibble, 7)) throw 'Invalid KNX 3-level mid group: ' + addr; - if (r(lonibble, 255)) throw 'Invalid KNX 3-level sub group: ' + addr; - address.writeUInt8((hinibble << 3) + midnibble, 0); - address.writeUInt8(lonibble, 1); - return address; - } - // 3 level physical address - if (r(hinibble, 15)) throw 'Invalid KNX area address: ' + addr; - if (r(midnibble, 15)) throw 'Invalid KNX line address: ' + addr; - if (r(lonibble, 255)) throw 'Invalid KNX device address: ' + addr; - address.writeUInt8((hinibble << 4) + midnibble, 0); - address.writeUInt8(lonibble, 1); - return address; -}; - + if (!addr) { + KnxLog.get().warn('Fix your code - no address given to Address.parse') + } + const group = addrtype === TYPE.GROUP + const address = Buffer.allocUnsafe(2) + const tokens = addr + .split(group ? '/' : '.') + .filter((w) => w.length > 0) + .map((w) => parseInt(w)) + if (tokens.length < 2) throw Error('Invalid address (less than 2 tokens)') + const [hinibble, midnibble, lonibble] = tokens + if (group && twoLevelAddressing) { + // 2 level group address + if (r(hinibble, 31)) + throw Error(`Invalid KNX 2-level main group: ${addr}`) + if (r(midnibble, 2047)) + throw Error(`Invalid KNX 2-level sub group: ${addr}`) + address.writeUInt16BE((hinibble << 11) + midnibble, 0) + return address + } + if (tokens.length < 3) throw Error('Invalid address - missing 3rd token') + if (group) { + // 3 level group address + if (r(hinibble, 31)) + throw Error(`Invalid KNX 3-level main group: ${addr}`) + if (r(midnibble, 7)) + throw Error(`Invalid KNX 3-level mid group: ${addr}`) + if (r(lonibble, 255)) + throw Error(`Invalid KNX 3-level sub group: ${addr}`) + address.writeUInt8((hinibble << 3) + midnibble, 0) + address.writeUInt8(lonibble, 1) + return address + } + // 3 level physical address + if (r(hinibble, 15)) throw Error(`Invalid KNX area address: ${addr}`) + if (r(midnibble, 15)) throw Error(`Invalid KNX line address: ${addr}`) + if (r(lonibble, 255)) throw Error(`Invalid KNX device address: ${addr}`) + address.writeUInt8((hinibble << 4) + midnibble, 0) + address.writeUInt8(lonibble, 1) + return address +} diff --git a/src/Datapoint.ts b/src/Datapoint.ts index fc23803..745a208 100644 --- a/src/Datapoint.ts +++ b/src/Datapoint.ts @@ -1,119 +1,127 @@ -import { EventEmitter } from 'events'; -import { format } from 'util'; -import * as DPTLib from './dptlib'; -import KnxLog from './KnxLog'; +import { EventEmitter } from 'events' +import { format } from 'util' +import * as DPTLib from './dptlib' +import KnxLog from './KnxLog' +import { hasProp } from './utils' interface Options { - ga: string; - dpt?: string; - autoread?: boolean; - status_ga?: string; + ga: string + dpt?: string + autoread?: boolean + status_ga?: string } class Datapoint extends EventEmitter { - private options: Options; - private dptid: string; - private dpt: any; - private current_value: any; - private conn: any; + private options: Options - constructor(options: Options, conn: any) { - if (options == null || options.ga == null) - throw new Error('must supply at least { ga, dpt }!'); - super(); + private dptid: string - this.options = options; - this.dptid = options.dpt || 'DPT1.001'; - this.dpt = DPTLib.resolve(this.dptid); - KnxLog.get().trace('resolved %s to %j', this.dptid, this.dpt); - this.current_value = null; - if (conn) this.bind(conn); - } + private dpt: any - bind(conn: any) { - if (!conn) throw new Error('must supply a valid KNX connection to bind to'); - this.conn = conn; - const gaevent = format('event_%s', this.options.ga); - conn.on(gaevent, (evt: string, src: any, buf: any) => { - switch (evt) { - case 'GroupValue_Write': - case 'GroupValue_Response': - if (buf) { - const jsvalue = DPTLib.fromBuffer(buf, this.dpt); - this.emit('event', evt, jsvalue); - this.update(jsvalue); - } - break; - default: - this.emit('event', evt); - } - }); - if (this.options.autoread) - if (conn.conntime) { - this.read(); - } else { - conn.on('connected', () => { - this.read(); - }); - } - } + private current_value: any - update(jsvalue: any) { - const old_value = this.current_value; - if (old_value === jsvalue) return; + private conn: any - this.emit('change', this.current_value, jsvalue, this.options.ga); - this.current_value = jsvalue; - const ts = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); - KnxLog.get().trace( - '%s **** %s DATAPOINT CHANGE (was: %j)', - ts, - this.toString(), - old_value - ); - } + constructor(options: Options, conn: any) { + if (options == null || options.ga == null) + throw new Error('must supply at least { ga, dpt }!') + super() - write(value: any) { - if (!this.conn) throw new Error('must supply a valid KNX connection to bind to'); - if (this.dpt.hasOwnProperty('range')) { - const { range } = this.dpt.basetype; - const [min, max] = range; - if (value < min || value > max) { - throw new Error( - format( - 'Value %j(%s) out of bounds(%j) for %s', - value, - typeof value, - range, - this.dptid - ) - ); - } - } - this.conn.write( - this.options.ga, - value, - this.dptid, - () => this.update(value) - ); - } + this.options = options + this.dptid = options.dpt || 'DPT1.001' + this.dpt = DPTLib.resolve(this.dptid) + KnxLog.get().trace('resolved %s to %j', this.dptid, this.dpt) + this.current_value = null + if (conn) this.bind(conn) + } - read(callback?: (src: any, jsvalue: any) => void) { - if (!this.conn) throw new Error('must supply a valid KNX connection to bind to'); - this.conn.read(this.options.ga, (src: any, buf: any) => { - const jsvalue = DPTLib.fromBuffer(buf, this.dpt); - if (typeof callback == 'function') callback(src, jsvalue); - }); - } + bind(conn: any) { + if (!conn) + throw new Error('must supply a valid KNX connection to bind to') + this.conn = conn + const gaevent = format('event_%s', this.options.ga) + conn.on(gaevent, (evt: string, src: any, buf: any) => { + switch (evt) { + case 'GroupValue_Write': + case 'GroupValue_Response': + if (buf) { + const jsvalue = DPTLib.fromBuffer(buf, this.dpt) + this.emit('event', evt, jsvalue) + this.update(jsvalue) + } + break + default: + this.emit('event', evt) + } + }) + if (this.options.autoread) + if (conn.conntime) { + this.read() + } else { + conn.on('connected', () => { + this.read() + }) + } + } - toString() { - return format( - '(%s) %s %s', - this.options.ga, - this.current_value, - (this.dpt.subtype && this.dpt.subtype.unit) || '' - ); - } + update(jsvalue: any) { + const old_value = this.current_value + if (old_value === jsvalue) return + + this.emit('change', this.current_value, jsvalue, this.options.ga) + this.current_value = jsvalue + const ts = new Date() + .toISOString() + .replace(/T/, ' ') + .replace(/\..+/, '') + KnxLog.get().trace( + '%s **** %s DATAPOINT CHANGE (was: %j)', + ts, + this.toString(), + old_value, + ) + } + + write(value: any) { + if (!this.conn) + throw new Error('must supply a valid KNX connection to bind to') + if (hasProp(this.dpt, 'range')) { + const { range } = this.dpt.basetype + const [min, max] = range + if (value < min || value > max) { + throw new Error( + format( + 'Value %j(%s) out of bounds(%j) for %s', + value, + typeof value, + range, + this.dptid, + ), + ) + } + } + this.conn.write(this.options.ga, value, this.dptid, () => + this.update(value), + ) + } + + read(callback?: (src: any, jsvalue: any) => void) { + if (!this.conn) + throw new Error('must supply a valid KNX connection to bind to') + this.conn.read(this.options.ga, (src: any, buf: any) => { + const jsvalue = DPTLib.fromBuffer(buf, this.dpt) + if (typeof callback === 'function') callback(src, jsvalue) + }) + } + + toString() { + return format( + '(%s) %s %s', + this.options.ga, + this.current_value, + (this.dpt.subtype && this.dpt.subtype.unit) || '', + ) + } } -export default Datapoint; \ No newline at end of file +export default Datapoint diff --git a/src/FSM.ts b/src/FSM.ts index 31efc4b..0963a83 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -1,1079 +1,1160 @@ -import os from "os"; -import util from "util"; -import * as ipaddr from "ipaddr.js"; -import machina from "machina"; -import { keyText, KnxConstants } from "./KnxConstants"; -import IpRoutingConnection from "./IpRoutingConnection"; -import IpTunnelingConnection from "./IpTunnelingConnection"; -import KnxLog, { KnxLogOptions } from "./KnxLog"; -import KnxNetProtocol from "./KnxProtocol"; -import { Writer } from "binary-protocol"; -import { Socket } from "dgram"; -import { populateAPDU } from "./dptlib"; -import { LogLevel } from "log-driver"; - -type KnxDeviceAddress = string; - -type KnxGroupAddress = string; +import os from 'os' +import util from 'util' +import * as ipaddr from 'ipaddr.js' +import machina from 'machina' +import { keyText, KnxConstants } from './KnxConstants' +import IpRoutingConnection from './IpRoutingConnection' +import IpTunnelingConnection from './IpTunnelingConnection' +import KnxLog, { KnxLogOptions } from './KnxLog' +import KnxNetProtocol from './KnxProtocol' +import { Writer } from 'binary-protocol' +import { Socket } from 'dgram' +import { populateAPDU } from './dptlib' +import { LogLevel } from 'log-driver' +import { hasProp } from './utils' + +type KnxDeviceAddress = string + +type KnxGroupAddress = string /** The type of the KnxValue depends on the DPT that it is associated with */ -type KnxValue = number | string | boolean | Date; +type KnxValue = number | string | boolean | Date type HandlersSpec = { - connected?: () => void; - disconnected?: () => void; - event?: ( - evt: string, - src: KnxDeviceAddress, - dest: KnxGroupAddress, - value: Buffer - ) => void; - error?: (connstatus: any) => void; -}; + connected?: () => void + disconnected?: () => void + event?: ( + evt: string, + src: KnxDeviceAddress, + dest: KnxGroupAddress, + value: Buffer, + ) => void + error?: (connstatus: any) => void +} export type KnxOptions = { - /** ip address of the KNX router or interface */ - ipAddr?: string; - /** port of the KNX router or interface */ - ipPort?: number; - /** in case you need to specify the multicast interface (say if you have more than one) */ - interface?: string; - /** the KNX physical address we'd like to use */ - physAddr?: string; - /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ - loglevel?: LogLevel; - /** do not automatically connect, but use connection.Connect() to establish connection */ - manualConnect?: boolean; - /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ - forceTunneling?: boolean; - /** wait at least 10 millisec between each datagram */ - minimumDelay?: number; - /** enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this */ - suppress_ack_ldatareq?: boolean; - /** 14/03/2020 In tunneling mode, echoes the sent message by emitting a new emitEvent, so other object with same group address, can receive the sent message. Default is false. */ - localEchoInTunneling?: boolean; - /** event handlers. You can also bind them later with connection.on(event, fn) */ - handlers?: HandlersSpec; -} & KnxLogOptions; + /** ip address of the KNX router or interface */ + ipAddr?: string + /** port of the KNX router or interface */ + ipPort?: number + /** in case you need to specify the multicast interface (say if you have more than one) */ + interface?: string + /** the KNX physical address we'd like to use */ + physAddr?: string + /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ + loglevel?: LogLevel + /** do not automatically connect, but use connection.Connect() to establish connection */ + manualConnect?: boolean + /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ + forceTunneling?: boolean + /** wait at least 10 millisec between each datagram */ + minimumDelay?: number + /** enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this */ + suppress_ack_ldatareq?: boolean + /** 14/03/2020 In tunneling mode, echoes the sent message by emitting a new emitEvent, so other object with same group address, can receive the sent message. Default is false. */ + localEchoInTunneling?: boolean + /** event handlers. You can also bind them later with connection.on(event, fn) */ + handlers?: HandlersSpec +} & KnxLogOptions export interface Datagram { - header_length: number; - protocol_version: number; - service_type: number; - total_length: number; - cemi?: { - dest_addr: string; - src_addr: string; - addinfo_length?: number; - apdu: { - apci: string; - data: any; - tpci: number; - bitlength?: number; - apdu_length?: number; - apdu_raw?: any; - }; - msgcode: number; - ctrl?: { - frameType: number; - reserved: number; - repeat: number; - broadcast: number; - priority: number; - acknowledge: number; - confirm: number; - destAddrType: number; - hopCount: number; - extendedFrame: number; - }; - }; - tunnstate?: { - seqnum?: number; - channel_id: number; - tunnel_endpoint: string; - rsvd?: number; - }; - tunn?: { - protocol_type: number; - tunnel_endpoint: string; - }; - hpai?: { - header_length?: number; - protocol_type: number; - tunnel_endpoint: string; - }; - connstate?: { - state: number; - channel_id: number; - status?: number; - }; - cri?: { - connection_type: number; - knx_layer: number; - unused: number; - }; + header_length: number + protocol_version: number + service_type: number + total_length: number + cemi?: { + dest_addr: string + src_addr: string + addinfo_length?: number + apdu: { + apci: string + data: any + tpci: number + bitlength?: number + apdu_length?: number + apdu_raw?: any + } + msgcode: number + ctrl?: { + frameType: number + reserved: number + repeat: number + broadcast: number + priority: number + acknowledge: number + confirm: number + destAddrType: number + hopCount: number + extendedFrame: number + } + } + tunnstate?: { + seqnum?: number + channel_id: number + tunnel_endpoint: string + rsvd?: number + } + tunn?: { + protocol_type: number + tunnel_endpoint: string + } + hpai?: { + header_length?: number + protocol_type: number + tunnel_endpoint: string + } + connstate?: { + state: number + channel_id: number + status?: number + } + cri?: { + connection_type: number + knx_layer: number + unused: number + } } const KnxFSM = machina.Fsm.extend({ - namespace: "knxnet", - initialState: "uninitialized", - states: { - uninitialized: { - ["*"]() { - this.transition("connecting"); - }, - }, - - jumptoconnecting: { - _onEnter() { - this.transition("connecting"); - }, - }, - - connecting: { - _onEnter() { - this.emit("disconnected"); - this.log.debug(util.format("useTunneling=%j", this.useTunneling)); - if (this.useTunneling) { - let connection_attempts = 0; - if (!this.localAddress) - throw "Not bound to an IPv4 non-loopback interface"; - this.log.debug( - util.format("Connecting via %s...", this.localAddress) - ); - this.connecttimer = setInterval(() => { - connection_attempts += 1; - if (connection_attempts >= 3) { - clearInterval(this.connecttimer); - if (this.remoteEndpoint.addr.range() == "multicast") { - this.log.warn( - "connection timed out, falling back to pure routing mode..." - ); - this.usingMulticastTunneling = true; - this.transition("connected"); - } else { - this.reconnection_cycles += 1; - const delay = Math.min(this.reconnection_cycles * 3, 300); - this.log.debug( - "reattempting connection in " + delay + " seconds" - ); - setTimeout( - () => this.transition("jumptoconnecting"), - delay * 1000 - ); - } - } else { - this.log.warn("connection timed out, retrying..."); - this.send( - this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) - ); - } - }, 3000); - delete this.channel_id; - delete this.conntime; - delete this.lastSentTime; - this.send( - this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) - ); - } else { - this.transition("connected"); - } - }, - _onExit() { - clearInterval(this.connecttimer); - }, - inbound_CONNECT_RESPONSE(datagram: any) { - this.log.debug(util.format("got connect response")); - if ( - datagram.hasOwnProperty("connstate") && - datagram.connstate.status === - KnxConstants.RESPONSECODE.E_NO_MORE_CONNECTIONS - ) { - try { - this.socket.close(); - } catch (error) {} - this.transition("uninitialized"); - this.emit("disconnected"); - this.log.debug( - "The KNXnet/IP server rejected the data connection (Maximum connections reached). Waiting 1 minute before retrying..." - ); - setTimeout(() => { - this.Connect(); - }, 60000); - } else { - this.channel_id = datagram.connstate.channel_id; - this.send( - this.prepareDatagram( - KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST - ) - ); - } - }, - inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { - if (this.useTunneling) { - const str = keyText("RESPONSECODE", datagram.connstate.status); - this.log.debug( - util.format( - "Got connection state response, connstate: %s, channel ID: %d", - str, - datagram.connstate.channel_id - ) - ); - this.transition("connected"); - } - }, - ["*"](data: any) { - this.log.debug(util.format("*** deferring Until Transition %j", data)); - this.deferUntilTransition("idle"); - }, - }, - - connected: { - _onEnter() { - this.reconnection_cycles = 0; - this.seqnum = -1; - this.lastSentTime = this.conntime = Date.now(); - this.log.debug( - util.format( - "--- Connected in %s mode ---", - this.useTunneling ? "TUNNELING" : "ROUTING" - ) - ); - this.transition("idle"); - this.emit("connected"); - }, - }, - - disconnecting: { - _onEnter() { - if (this.useTunneling) { - const aliveFor = this.conntime ? Date.now() - this.conntime : 0; - KnxLog.get().debug( - "(%s):\tconnection alive for %d seconds", - this.compositeState(), - aliveFor / 1000 - ); - this.disconnecttimer = setTimeout(() => { - KnxLog.get().debug( - "(%s):\tconnection timed out", - this.compositeState() - ); - try { - this.socket.close(); - } catch (error) {} - this.transition("uninitialized"); - this.emit("disconnected"); - }, 3000); - this.send( - this.prepareDatagram(KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST), - (err: any) => { - KnxLog.get().debug( - "(%s):\tsent DISCONNECT_REQUEST", - this.compositeState() - ); - } - ); - } - }, - _onExit() { - clearTimeout(this.disconnecttimer); - }, - inbound_DISCONNECT_RESPONSE(datagram: any) { - if (this.useTunneling) { - KnxLog.get().debug( - "(%s):\tgot disconnect response", - this.compositeState() - ); - try { - this.socket.close(); - } catch (error) {} - this.transition("uninitialized"); - this.emit("disconnected"); - } - }, - }, - - idle: { - _onEnter() { - if (this.useTunneling) { - if (this.idletimer == null) { - this.idletimer = setTimeout(() => { - this.transition("requestingConnState"); - clearTimeout(this.idletimer); - this.idletimer = null; - }, 60000); - } - } - KnxLog.get().debug("(%s):\t%s", this.compositeState(), " zzzz..."); - this.processQueue(); - }, - _onExit() {}, - outbound_ROUTING_INDICATION(datagram: Datagram) { - const elapsed = Date.now() - this.lastSentTime; - if ( - !this.options.minimumDelay || - elapsed >= this.options.minimumDelay - ) { - this.transition("sendDatagram", datagram); - } else { - setTimeout( - () => this.handle("outbound_ROUTING_INDICATION", datagram), - this.minimumDelay - elapsed - ); - } - }, - outbound_TUNNELING_REQUEST(datagram: Datagram) { - if (this.useTunneling) { - const elapsed = Date.now() - this.lastSentTime; - if ( - !this.options.minimumDelay || - elapsed >= this.options.minimumDelay - ) { - this.transition("sendDatagram", datagram); - } else { - setTimeout( - () => this.handle("outbound_TUNNELING_REQUEST", datagram), - this.minimumDelay - elapsed - ); - } - } else { - KnxLog.get().debug( - "(%s):\tdropping outbound TUNNELING_REQUEST, we're in routing mode", - this.compositeState() - ); - } - }, - ["inbound_TUNNELING_REQUEST_L_Data.ind"](datagram: Datagram) { - if (this.useTunneling) { - this.transition("recvTunnReqIndication", datagram); - } - }, - ["inbound_TUNNELING_REQUEST_L_Data.con"](datagram: Datagram) { - if (this.useTunneling) { - const confirmed = this.sentTunnRequests[datagram.cemi.dest_addr]; - if (confirmed) { - delete this.sentTunnRequests[datagram.cemi.dest_addr]; - this.emit("confirmed", confirmed); - } - KnxLog.get().trace( - "(%s): %s %s", - this.compositeState(), - datagram.cemi.dest_addr, - confirmed - ? "delivery confirmation (L_Data.con) received" - : "unknown dest addr" - ); - this.acknowledge(datagram); - } - }, - ["inbound_ROUTING_INDICATION_L_Data.ind"](datagram: Datagram) { - this.emitEvent(datagram); - }, - inbound_DISCONNECT_REQUEST(datagram: any) { - if (this.useTunneling) { - this.transition("connecting"); - } - }, - }, - - requestingConnState: { - _onEnter() { - KnxLog.get().debug("Requesting Connection State"); - KnxLog.get().trace( - "(%s): Requesting Connection State", - this.compositeState() - ); - this.send( - this.prepareDatagram( - KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST - ) - ); - this.connstatetimer = setTimeout(() => { - const msg = "timed out waiting for CONNECTIONSTATE_RESPONSE"; - KnxLog.get().trace("(%s): %s", this.compositeState(), msg); - this.transition("connecting"); - this.emit("error", msg); - }, 1000); - }, - _onExit() { - clearTimeout(this.connstatetimer); - }, - inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { - const state = keyText("RESPONSECODE", datagram.connstate.status); - switch (datagram.connstate.status) { - case 0: - this.transition("idle"); - break; - default: - this.log.debug( - util.format( - "*** error: %s *** (connstate.code: %d)", - state, - datagram.connstate.status - ) - ); - this.transition("connecting"); - this.emit("error", state); - } - }, - ["*"](data: any) { - this.log.debug( - util.format( - "*** deferring %s until transition from requestingConnState => idle", - data.inputType - ) - ); - this.deferUntilTransition("idle"); - }, - }, - - sendDatagram: { - _onEnter(datagram: Datagram) { - this.seqnum += 1; - if (this.useTunneling) datagram.tunnstate.seqnum = this.seqnum & 0xff; - this.send(datagram, (err: any) => { - if (err) { - this.seqnum -= 1; - this.transition("idle"); - } else { - if (this.useTunneling) - this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; - this.lastSentTime = Date.now(); - this.log.debug( - "(%s):\t>>>>>>> successfully sent seqnum: %d", - this.compositeState(), - this.seqnum - ); - if (this.useTunneling) { - this.transition("sendTunnReq_waitACK", datagram); - } else { - this.transition("idle"); - } - } - }); - }, - ["*"](data: any) { - this.log.debug( - util.format( - "*** deferring %s until transition sendDatagram => idle", - data.inputType - ) - ); - this.deferUntilTransition("idle"); - }, - }, - sendTunnReq_waitACK: { - _onEnter(datagram: Datagram) { - this.tunnelingAckTimer = setTimeout(() => { - this.log.debug("timed out waiting for TUNNELING_ACK"); - this.transition("idle"); - this.emit("tunnelreqfailed", datagram); - }, 2000); - }, - _onExit() { - clearTimeout(this.tunnelingAckTimer); - }, - inbound_TUNNELING_ACK(datagram: Datagram) { - this.log.debug( - util.format( - "===== datagram %d acknowledged by IP router", - datagram.tunnstate.seqnum - ) - ); - this.transition("idle"); - }, - ["*"](data: any) { - this.log.debug( - util.format( - "*** deferring %s until transition sendTunnReq_waitACK => idle", - data.inputType - ) - ); - this.deferUntilTransition("idle"); - }, - }, - recvTunnReqIndication: { - _onEnter(datagram: Datagram) { - this.seqnumRecv = datagram.tunnstate.seqnum; - this.acknowledge(datagram); - this.transition("idle"); - this.emitEvent(datagram); - }, - ["*"](data: any) { - this.log.debug(util.format("*** deferring Until Transition %j", data)); - this.deferUntilTransition("idle"); - }, - }, - }, -}); + namespace: 'knxnet', + initialState: 'uninitialized', + states: { + uninitialized: { + '*': function () { + this.transition('connecting') + }, + }, + + jumptoconnecting: { + _onEnter() { + this.transition('connecting') + }, + }, + + connecting: { + _onEnter() { + this.emit('disconnected') + this.log.debug( + util.format('useTunneling=%j', this.useTunneling), + ) + if (this.useTunneling) { + let connection_attempts = 0 + if (!this.localAddress) + throw Error( + 'Not bound to an IPv4 non-loopback interface', + ) + this.log.debug( + util.format('Connecting via %s...', this.localAddress), + ) + this.connecttimer = setInterval(() => { + connection_attempts += 1 + if (connection_attempts >= 3) { + clearInterval(this.connecttimer) + if ( + this.remoteEndpoint.addr.range() === 'multicast' + ) { + this.log.warn( + 'connection timed out, falling back to pure routing mode...', + ) + this.usingMulticastTunneling = true + this.transition('connected') + } else { + this.reconnection_cycles += 1 + const delay = Math.min( + this.reconnection_cycles * 3, + 300, + ) + this.log.debug( + `reattempting connection in ${delay} seconds`, + ) + setTimeout( + () => this.transition('jumptoconnecting'), + delay * 1000, + ) + } + } else { + this.log.warn('connection timed out, retrying...') + this.send( + this.prepareDatagram( + KnxConstants.SERVICE_TYPE.CONNECT_REQUEST, + ), + ) + } + }, 3000) + delete this.channel_id + delete this.conntime + delete this.lastSentTime + this.send( + this.prepareDatagram( + KnxConstants.SERVICE_TYPE.CONNECT_REQUEST, + ), + ) + } else { + this.transition('connected') + } + }, + _onExit() { + clearInterval(this.connecttimer) + }, + inbound_CONNECT_RESPONSE(datagram: any) { + this.log.debug(util.format('got connect response')) + if ( + hasProp(datagram, 'connstate') && + datagram.connstate.status === + KnxConstants.RESPONSECODE.E_NO_MORE_CONNECTIONS + ) { + try { + this.socket.close() + } catch (error) { + // noop + } + this.transition('uninitialized') + this.emit('disconnected') + this.log.debug( + 'The KNXnet/IP server rejected the data connection (Maximum connections reached). Waiting 1 minute before retrying...', + ) + setTimeout(() => { + this.Connect() + }, 60000) + } else { + this.channel_id = datagram.connstate.channel_id + this.send( + this.prepareDatagram( + KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST, + ), + ) + } + }, + inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { + if (this.useTunneling) { + const str = keyText( + 'RESPONSECODE', + datagram.connstate.status, + ) + this.log.debug( + util.format( + 'Got connection state response, connstate: %s, channel ID: %d', + str, + datagram.connstate.channel_id, + ), + ) + this.transition('connected') + } + }, + '*': function (data: any) { + this.log.debug( + util.format('*** deferring Until Transition %j', data), + ) + this.deferUntilTransition('idle') + }, + }, + + connected: { + _onEnter() { + this.reconnection_cycles = 0 + this.seqnum = -1 + this.lastSentTime = Date.now() + this.conntime = this.lastSentTime + this.log.debug( + util.format( + '--- Connected in %s mode ---', + this.useTunneling ? 'TUNNELING' : 'ROUTING', + ), + ) + this.transition('idle') + this.emit('connected') + }, + }, + + disconnecting: { + _onEnter() { + if (this.useTunneling) { + const aliveFor = this.conntime + ? Date.now() - this.conntime + : 0 + KnxLog.get().debug( + '(%s):\tconnection alive for %d seconds', + this.compositeState(), + aliveFor / 1000, + ) + this.disconnecttimer = setTimeout(() => { + KnxLog.get().debug( + '(%s):\tconnection timed out', + this.compositeState(), + ) + try { + this.socket.close() + } catch (error) { + // noop + } + this.transition('uninitialized') + this.emit('disconnected') + }, 3000) + this.send( + this.prepareDatagram( + KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST, + ), + (err: any) => { + KnxLog.get().debug( + '(%s):\tsent DISCONNECT_REQUEST', + this.compositeState(), + ) + }, + ) + } + }, + _onExit() { + clearTimeout(this.disconnecttimer) + }, + inbound_DISCONNECT_RESPONSE(datagram: any) { + if (this.useTunneling) { + KnxLog.get().debug( + '(%s):\tgot disconnect response', + this.compositeState(), + ) + try { + this.socket.close() + } catch (error) { + // noop + } + this.transition('uninitialized') + this.emit('disconnected') + } + }, + }, + + idle: { + _onEnter() { + if (this.useTunneling) { + if (this.idletimer == null) { + this.idletimer = setTimeout(() => { + this.transition('requestingConnState') + clearTimeout(this.idletimer) + this.idletimer = null + }, 60000) + } + } + KnxLog.get().debug( + '(%s):\t%s', + this.compositeState(), + ' zzzz...', + ) + this.processQueue() + }, + _onExit() {}, + outbound_ROUTING_INDICATION(datagram: Datagram) { + const elapsed = Date.now() - this.lastSentTime + if ( + !this.options.minimumDelay || + elapsed >= this.options.minimumDelay + ) { + this.transition('sendDatagram', datagram) + } else { + setTimeout( + () => + this.handle( + 'outbound_ROUTING_INDICATION', + datagram, + ), + this.minimumDelay - elapsed, + ) + } + }, + outbound_TUNNELING_REQUEST(datagram: Datagram) { + if (this.useTunneling) { + const elapsed = Date.now() - this.lastSentTime + if ( + !this.options.minimumDelay || + elapsed >= this.options.minimumDelay + ) { + this.transition('sendDatagram', datagram) + } else { + setTimeout( + () => + this.handle( + 'outbound_TUNNELING_REQUEST', + datagram, + ), + this.minimumDelay - elapsed, + ) + } + } else { + KnxLog.get().debug( + "(%s):\tdropping outbound TUNNELING_REQUEST, we're in routing mode", + this.compositeState(), + ) + } + }, + 'inbound_TUNNELING_REQUEST_L_Data.ind': function ( + datagram: Datagram, + ) { + if (this.useTunneling) { + this.transition('recvTunnReqIndication', datagram) + } + }, + 'inbound_TUNNELING_REQUEST_L_Data.con': function ( + datagram: Datagram, + ) { + if (this.useTunneling) { + const confirmed = + this.sentTunnRequests[datagram.cemi.dest_addr] + if (confirmed) { + delete this.sentTunnRequests[datagram.cemi.dest_addr] + this.emit('confirmed', confirmed) + } + KnxLog.get().trace( + '(%s): %s %s', + this.compositeState(), + datagram.cemi.dest_addr, + confirmed + ? 'delivery confirmation (L_Data.con) received' + : 'unknown dest addr', + ) + this.acknowledge(datagram) + } + }, + 'inbound_ROUTING_INDICATION_L_Data.ind': function ( + datagram: Datagram, + ) { + this.emitEvent(datagram) + }, + inbound_DISCONNECT_REQUEST(datagram: any) { + if (this.useTunneling) { + this.transition('connecting') + } + }, + }, + + requestingConnState: { + _onEnter() { + KnxLog.get().debug('Requesting Connection State') + KnxLog.get().trace( + '(%s): Requesting Connection State', + this.compositeState(), + ) + this.send( + this.prepareDatagram( + KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST, + ), + ) + this.connstatetimer = setTimeout(() => { + const msg = 'timed out waiting for CONNECTIONSTATE_RESPONSE' + KnxLog.get().trace('(%s): %s', this.compositeState(), msg) + this.transition('connecting') + this.emit('error', msg) + }, 1000) + }, + _onExit() { + clearTimeout(this.connstatetimer) + }, + inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { + const state = keyText('RESPONSECODE', datagram.connstate.status) + switch (datagram.connstate.status) { + case 0: + this.transition('idle') + break + default: + this.log.debug( + util.format( + '*** error: %s *** (connstate.code: %d)', + state, + datagram.connstate.status, + ), + ) + this.transition('connecting') + this.emit('error', state) + } + }, + '*': function (data: any) { + this.log.debug( + util.format( + '*** deferring %s until transition from requestingConnState => idle', + data.inputType, + ), + ) + this.deferUntilTransition('idle') + }, + }, + + sendDatagram: { + _onEnter(datagram: Datagram) { + this.seqnum += 1 + if (this.useTunneling) + datagram.tunnstate.seqnum = this.seqnum & 0xff + this.send(datagram, (err: any) => { + if (err) { + this.seqnum -= 1 + this.transition('idle') + } else { + if (this.useTunneling) + this.sentTunnRequests[datagram.cemi.dest_addr] = + datagram + this.lastSentTime = Date.now() + this.log.debug( + '(%s):\t>>>>>>> successfully sent seqnum: %d', + this.compositeState(), + this.seqnum, + ) + if (this.useTunneling) { + this.transition('sendTunnReq_waitACK', datagram) + } else { + this.transition('idle') + } + } + }) + }, + '*': function (data: any) { + this.log.debug( + util.format( + '*** deferring %s until transition sendDatagram => idle', + data.inputType, + ), + ) + this.deferUntilTransition('idle') + }, + }, + sendTunnReq_waitACK: { + _onEnter(datagram: Datagram) { + this.tunnelingAckTimer = setTimeout(() => { + this.log.debug('timed out waiting for TUNNELING_ACK') + this.transition('idle') + this.emit('tunnelreqfailed', datagram) + }, 2000) + }, + _onExit() { + clearTimeout(this.tunnelingAckTimer) + }, + inbound_TUNNELING_ACK(datagram: Datagram) { + this.log.debug( + util.format( + '===== datagram %d acknowledged by IP router', + datagram.tunnstate.seqnum, + ), + ) + this.transition('idle') + }, + '*': function (data: any) { + this.log.debug( + util.format( + '*** deferring %s until transition sendTunnReq_waitACK => idle', + data.inputType, + ), + ) + this.deferUntilTransition('idle') + }, + }, + recvTunnReqIndication: { + _onEnter(datagram: Datagram) { + this.seqnumRecv = datagram.tunnstate.seqnum + this.acknowledge(datagram) + this.transition('idle') + this.emitEvent(datagram) + }, + '*': function (data: any) { + this.log.debug( + util.format('*** deferring Until Transition %j', data), + ) + this.deferUntilTransition('idle') + }, + }, + }, +}) export class KnxFSMConnection extends KnxFSM { - private options: KnxOptions; - private log: any; - private ThreeLevelGroupAddressing: boolean; - private reconnection_cycles: number; - private sentTunnRequests: { [key: string]: Datagram }; - private useTunneling: boolean; - private remoteEndpoint: { - addrstring: string; - addr: any; - port: number; - }; - private localEchoInTunneling: boolean | undefined; - private channel_id?: any; - private conntime?: number; - private lastSentTime?: number; - private connecttimer?: NodeJS.Timeout; - private disconnecttimer?: NodeJS.Timeout; - private connstatetimer?: NodeJS.Timeout; - private idletimer?: NodeJS.Timeout; - private tunnelingAckTimer?: NodeJS.Timeout; - private seqnum: number; - private seqnumRecv: number; - - private writer: Writer; - private socket: Socket; - - public localAddress: string | null; - - constructor(options: KnxOptions) { - super(); - - this.options = options || {}; - this.log = KnxLog.get(options); - this.localAddress = null; - this.ThreeLevelGroupAddressing = true; - this.reconnection_cycles = 0; - this.sentTunnRequests = {}; - this.useTunneling = options.forceTunneling || false; - this.remoteEndpoint = { - addrstring: options.ipAddr || "224.0.23.12", - addr: ipaddr.parse(options.ipAddr || "224.0.23.12"), - port: options.ipPort || 3671, - }; - const range = this.remoteEndpoint.addr.range(); - this.localEchoInTunneling = - typeof options.localEchoInTunneling !== "undefined" - ? options.localEchoInTunneling - : false; - this.log.debug( - "initializing %s connection to %s", - range, - this.remoteEndpoint.addrstring - ); - switch (range) { - case "multicast": - if (this.localEchoInTunneling) { - this.localEchoInTunneling = false; - this.log.debug( - "localEchoInTunneling: true but DISABLED because i am on multicast" - ); - } - IpRoutingConnection(this); - break; - case "unicast": - case "private": - case "loopback": - this.useTunneling = true; - IpTunnelingConnection(this); - break; - default: - throw util.format( - "IP address % (%s) cannot be used for KNX", - options.ipAddr, - range - ); - } - - if (typeof options.handlers === "object") { - for (const [key, value] of Object.entries(options.handlers)) { - if (typeof value === "function") { - this.on(key, value); - } - } - } - // boot up the KNX connection unless told otherwise - if (!options.manualConnect) this.Connect(); - } - - /** - * -------------------------------- - * KNX FSM Utils methods - * -------------------------------- - */ - - acknowledge(datagram: Datagram) { - const ack = this.prepareDatagram(KnxConstants.SERVICE_TYPE.TUNNELING_ACK); - ack.tunnstate.seqnum = datagram.tunnstate.seqnum; - this.send(ack, (err: any) => {}); - } - - emitEvent(datagram: Datagram) { - const evtName = datagram.cemi.apdu.apci; - this.emit( - util.format("event_%s", datagram.cemi.dest_addr), - evtName, - datagram.cemi.src_addr, - datagram.cemi.apdu.data - ); - this.emit( - util.format("%s_%s", evtName, datagram.cemi.dest_addr), - datagram.cemi.src_addr, - datagram.cemi.apdu.data - ); - this.emit( - evtName, - datagram.cemi.src_addr, - datagram.cemi.dest_addr, - datagram.cemi.apdu.data - ); - this.emit( - "event", - evtName, - datagram.cemi.src_addr, - datagram.cemi.dest_addr, - datagram.cemi.apdu.data - ); - } - - getLocalAddress() { - const candidateInterfaces = this.getIPv4Interfaces(); - if (this.options && this.options.interface) { - const iface = candidateInterfaces[this.options.interface]; - if (!iface) - throw new Error( - "Interface " + - this.options.interface + - " not found or has no useful IPv4 address!" - ); - - return candidateInterfaces[this.options.interface].address; - } - const first = Object.values(candidateInterfaces)[0]; - if (first) return first.address; - - throw "No valid IPv4 interfaces detected"; - } - - getIPv4Interfaces() { - const candidateInterfaces: { [key: string]: any } = {}; - const interfaces = os.networkInterfaces(); - for (const [iface, addrs] of Object.entries(interfaces)) { - for (const addr of addrs) { - if ([4, "IPv4"].indexOf(addr.family) > -1 && !addr.internal) { - this.log.trace( - util.format("candidate interface: %s (%j)", iface, addr) - ); - candidateInterfaces[iface] = addr; - } - } - } - return candidateInterfaces; - } - - BindSocket(cb: (socket: any) => void) { - // THIS IS A STUB and should be overridden by the connection type - } - - Connect() { - // THIS IS A STUB and should be overridden by the connection type - } - - /** - * -------------------------------- - * Connection management methods - * -------------------------------- - */ - - /** Bind incoming UDP packet handler */ - onUdpSocketMessage(msg: Buffer, rinfo: any, callback: () => void): void { - // get the incoming packet's service type ... - try { - const reader = KnxNetProtocol.createReader(msg); - // TODO: improve types for binary protocol - reader.KNXNetHeader("tmp"); - const dg = reader.next()["tmp"]; - const descr = datagramDesc(dg); - KnxLog.get().trace( - "(%s): Received %s message: %j", - this.compositeState(), - descr, - dg - ); - if ( - !isNaN(this.channel_id) && - ((dg.hasOwnProperty("connstate") && - dg.connstate.channel_id != this.channel_id) || - (dg.hasOwnProperty("tunnstate") && - dg.tunnstate.channel_id != this.channel_id)) - ) { - KnxLog.get().trace( - "(%s): *** Ignoring %s datagram for other channel (own: %d)", - this.compositeState(), - descr, - this.channel_id - ); - } else { - // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") - const signal = util.format("inbound_%s", descr); - if (descr === "DISCONNECT_REQUEST") { - KnxLog.get().info("empty internal fsm queue due to %s: ", signal); - this.clearQueue(); - } - this.handle(signal, dg); - } - } catch (err) { - KnxLog.get().debug( - "(%s): Incomplete/unparseable UDP packet: %s: %s", - this.compositeState(), - err, - msg.toString("hex") - ); - } - } - - AddConnState(datagram: Datagram): void { - datagram.connstate = { - channel_id: this.channel_id, - state: 0, - }; - } - - AddTunnState(datagram: Datagram): void { - // add the remote IP router's endpoint - datagram.tunnstate = { - channel_id: this.channel_id, - tunnel_endpoint: - this.remoteEndpoint.addr + ":" + this.remoteEndpoint.port, - }; - } - - AddCRI = (datagram: Datagram): void => { - // add the CRI - datagram.cri = { - connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, - knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, - unused: 0, - }; - }; - - AddCEMI(datagram: Datagram, msgcode?: number): void { - const sendAck = - (msgcode || 0x11) == 0x11 && !this.options.suppress_ack_ldatareq; // only for L_Data.req - datagram.cemi = { - msgcode: msgcode || 0x11, // default: L_Data.req for tunneling - ctrl: { - frameType: 1, // 0=extended 1=standard - reserved: 0, // always 0 - repeat: 1, // the OPPOSITE: 1=do NOT repeat - broadcast: 1, // 0-system broadcast 1-broadcast - priority: 3, // 0-system 1-normal 2-urgent 3-low - acknowledge: sendAck ? 1 : 0, - confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error - // 2nd byte - destAddrType: 1, // FIXME: 0-physical 1-groupaddr - hopCount: 6, - extendedFrame: 0, - }, - src_addr: this.options.physAddr || "15.15.15", - dest_addr: "0/0/0", // - apdu: { - // default operation is GroupValue_Write - apci: "GroupValue_Write", - tpci: 0, - data: 0, - }, - }; - } - - /* - * submit an outbound request to the state machine - * - * type: service type - * datagram_template: - * if a datagram is passed, use this as - * if a function is passed, use this to DECORATE - * if NULL, then just make a new empty datagram. Look at AddXXX methods - */ - Request( - type: number, - datagram_template: (datagram: Datagram) => void, - callback?: () => void - ): void { - // populate skeleton datagram - const datagram = this.prepareDatagram(type); - // decorate the datagram, if a function is passed - if (typeof datagram_template == "function") { - datagram_template(datagram); - } - // make sure that we override the datagram service type! - datagram.service_type = type; - const st = keyText("SERVICE_TYPE", type); - // hand off the outbound request to the state machine - this.handle("outbound_" + st, datagram); - if (typeof callback === "function") callback(); - } - - // prepare a datagram for the given service type - prepareDatagram(svcType: number): Datagram { - const datagram: Datagram = { - header_length: 6, - protocol_version: 16, // 0x10 == version 1.0 - service_type: svcType, - total_length: null, // filled in automatically - }; - // - AddHPAI(datagram); - // - switch (svcType) { - case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: - AddTunn(datagram); - this.AddCRI(datagram); // no break! - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: - case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: - this.AddConnState(datagram); - break; - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - this.AddCEMI(datagram, KnxConstants.MESSAGECODES["L_Data.ind"]); - break; - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - AddTunn(datagram); - this.AddTunnState(datagram); - this.AddCEMI(datagram); - break; - case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - this.AddTunnState(datagram); - break; - default: - KnxLog.get().debug("Do not know how to deal with svc type %d", svcType); - } - return datagram; - } - - /* + private options: KnxOptions + + private log: any + + private ThreeLevelGroupAddressing: boolean + + private reconnection_cycles: number + + private sentTunnRequests: { [key: string]: Datagram } + + private useTunneling: boolean + + private remoteEndpoint: { + addrstring: string + addr: any + port: number + } + + private localEchoInTunneling: boolean | undefined + + private channel_id?: any + + private conntime?: number + + private lastSentTime?: number + + private connecttimer?: NodeJS.Timeout + + private disconnecttimer?: NodeJS.Timeout + + private connstatetimer?: NodeJS.Timeout + + private idletimer?: NodeJS.Timeout + + private tunnelingAckTimer?: NodeJS.Timeout + + private seqnum: number + + private seqnumRecv: number + + private writer: Writer + + private socket: Socket + + public localAddress: string | null + + constructor(options: KnxOptions) { + super() + + this.options = options || {} + this.log = KnxLog.get(options) + this.localAddress = null + this.ThreeLevelGroupAddressing = true + this.reconnection_cycles = 0 + this.sentTunnRequests = {} + this.useTunneling = options.forceTunneling || false + this.remoteEndpoint = { + addrstring: options.ipAddr || '224.0.23.12', + addr: ipaddr.parse(options.ipAddr || '224.0.23.12'), + port: options.ipPort || 3671, + } + const range = this.remoteEndpoint.addr.range() + this.localEchoInTunneling = + typeof options.localEchoInTunneling !== 'undefined' + ? options.localEchoInTunneling + : false + this.log.debug( + 'initializing %s connection to %s', + range, + this.remoteEndpoint.addrstring, + ) + switch (range) { + case 'multicast': + if (this.localEchoInTunneling) { + this.localEchoInTunneling = false + this.log.debug( + 'localEchoInTunneling: true but DISABLED because i am on multicast', + ) + } + IpRoutingConnection(this) + break + case 'unicast': + case 'private': + case 'loopback': + this.useTunneling = true + IpTunnelingConnection(this) + break + default: + throw Error( + util.format( + 'IP address % (%s) cannot be used for KNX', + options.ipAddr, + range, + ), + ) + } + + if (typeof options.handlers === 'object') { + for (const [key, value] of Object.entries(options.handlers)) { + if (typeof value === 'function') { + this.on(key, value) + } + } + } + // boot up the KNX connection unless told otherwise + if (!options.manualConnect) this.Connect() + } + + /** + * -------------------------------- + * KNX FSM Utils methods + * -------------------------------- + */ + + acknowledge(datagram: Datagram) { + const ack = this.prepareDatagram( + KnxConstants.SERVICE_TYPE.TUNNELING_ACK, + ) + ack.tunnstate.seqnum = datagram.tunnstate.seqnum + this.send(ack, (err: any) => {}) + } + + emitEvent(datagram: Datagram) { + const evtName = datagram.cemi.apdu.apci + this.emit( + util.format('event_%s', datagram.cemi.dest_addr), + evtName, + datagram.cemi.src_addr, + datagram.cemi.apdu.data, + ) + this.emit( + util.format('%s_%s', evtName, datagram.cemi.dest_addr), + datagram.cemi.src_addr, + datagram.cemi.apdu.data, + ) + this.emit( + evtName, + datagram.cemi.src_addr, + datagram.cemi.dest_addr, + datagram.cemi.apdu.data, + ) + this.emit( + 'event', + evtName, + datagram.cemi.src_addr, + datagram.cemi.dest_addr, + datagram.cemi.apdu.data, + ) + } + + getLocalAddress() { + const candidateInterfaces = this.getIPv4Interfaces() + if (this.options && this.options.interface) { + const iface = candidateInterfaces[this.options.interface] + if (!iface) + throw new Error( + `Interface ${this.options.interface} not found or has no useful IPv4 address!`, + ) + + return candidateInterfaces[this.options.interface].address + } + const first = Object.values(candidateInterfaces)[0] + if (first) return first.address + + throw Error('No valid IPv4 interfaces detected') + } + + getIPv4Interfaces() { + const candidateInterfaces: { [key: string]: any } = {} + const interfaces = os.networkInterfaces() + for (const [iface, addrs] of Object.entries(interfaces)) { + for (const addr of addrs) { + if ([4, 'IPv4'].indexOf(addr.family) > -1 && !addr.internal) { + this.log.trace( + util.format( + 'candidate interface: %s (%j)', + iface, + addr, + ), + ) + candidateInterfaces[iface] = addr + } + } + } + return candidateInterfaces + } + + BindSocket(cb: (socket: any) => void) { + // THIS IS A STUB and should be overridden by the connection type + } + + Connect() { + // THIS IS A STUB and should be overridden by the connection type + } + + /** + * -------------------------------- + * Connection management methods + * -------------------------------- + */ + + /** Bind incoming UDP packet handler */ + onUdpSocketMessage(msg: Buffer, rinfo: any, callback: () => void): void { + // get the incoming packet's service type ... + try { + const reader = KnxNetProtocol.createReader(msg) + // TODO: improve types for binary protocol + reader.KNXNetHeader('tmp') + const dg = reader.next()['tmp'] + const descr = datagramDesc(dg) + KnxLog.get().trace( + '(%s): Received %s message: %j', + this.compositeState(), + descr, + dg, + ) + if ( + !isNaN(this.channel_id) && + ((hasProp(dg, 'connstate') && + dg.connstate.channel_id !== this.channel_id) || + (hasProp(dg, 'tunnstate') && + dg.tunnstate.channel_id !== this.channel_id)) + ) { + KnxLog.get().trace( + '(%s): *** Ignoring %s datagram for other channel (own: %d)', + this.compositeState(), + descr, + this.channel_id, + ) + } else { + // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") + const signal = util.format('inbound_%s', descr) + if (descr === 'DISCONNECT_REQUEST') { + KnxLog.get().info( + 'empty internal fsm queue due to %s: ', + signal, + ) + this.clearQueue() + } + this.handle(signal, dg) + } + } catch (err) { + KnxLog.get().debug( + '(%s): Incomplete/unparseable UDP packet: %s: %s', + this.compositeState(), + err, + msg.toString('hex'), + ) + } + } + + AddConnState(datagram: Datagram): void { + datagram.connstate = { + channel_id: this.channel_id, + state: 0, + } + } + + AddTunnState(datagram: Datagram): void { + // add the remote IP router's endpoint + datagram.tunnstate = { + channel_id: this.channel_id, + tunnel_endpoint: `${this.remoteEndpoint.addr}:${this.remoteEndpoint.port}`, + } + } + + AddCRI = (datagram: Datagram): void => { + // add the CRI + datagram.cri = { + connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, + knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, + unused: 0, + } + } + + AddCEMI(datagram: Datagram, msgcode?: number): void { + const sendAck = + (msgcode || 0x11) === 0x11 && !this.options.suppress_ack_ldatareq // only for L_Data.req + datagram.cemi = { + msgcode: msgcode || 0x11, // default: L_Data.req for tunneling + ctrl: { + frameType: 1, // 0=extended 1=standard + reserved: 0, // always 0 + repeat: 1, // the OPPOSITE: 1=do NOT repeat + broadcast: 1, // 0-system broadcast 1-broadcast + priority: 3, // 0-system 1-normal 2-urgent 3-low + acknowledge: sendAck ? 1 : 0, + confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error + // 2nd byte + destAddrType: 1, // FIXME: 0-physical 1-groupaddr + hopCount: 6, + extendedFrame: 0, + }, + src_addr: this.options.physAddr || '15.15.15', + dest_addr: '0/0/0', // + apdu: { + // default operation is GroupValue_Write + apci: 'GroupValue_Write', + tpci: 0, + data: 0, + }, + } + } + + /* + * submit an outbound request to the state machine + * + * type: service type + * datagram_template: + * if a datagram is passed, use this as + * if a function is passed, use this to DECORATE + * if NULL, then just make a new empty datagram. Look at AddXXX methods + */ + Request( + type: number, + datagram_template: (datagram: Datagram) => void, + callback?: () => void, + ): void { + // populate skeleton datagram + const datagram = this.prepareDatagram(type) + // decorate the datagram, if a function is passed + if (typeof datagram_template === 'function') { + datagram_template(datagram) + } + // make sure that we override the datagram service type! + datagram.service_type = type + const st = keyText('SERVICE_TYPE', type) + // hand off the outbound request to the state machine + this.handle(`outbound_${st}`, datagram) + if (typeof callback === 'function') callback() + } + + // prepare a datagram for the given service type + prepareDatagram(svcType: number): Datagram { + const datagram: Datagram = { + header_length: 6, + protocol_version: 16, // 0x10 == version 1.0 + service_type: svcType, + total_length: null, // filled in automatically + } + // + AddHPAI(datagram) + // + switch (svcType) { + case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: + AddTunn(datagram) + this.AddCRI(datagram) // no break! + // eslint-disable-next-line no-fallthrough + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: + case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: + this.AddConnState(datagram) + break + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + this.AddCEMI(datagram, KnxConstants.MESSAGECODES['L_Data.ind']) + break + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + AddTunn(datagram) + this.AddTunnState(datagram) + this.AddCEMI(datagram) + break + case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: + this.AddTunnState(datagram) + break + default: + KnxLog.get().debug( + 'Do not know how to deal with svc type %d', + svcType, + ) + } + return datagram + } + + /* send the datagram over the wire */ - send(datagram: Datagram, callback: (err?: Error) => void): void { - let cemitype: string; // TODO: set, but unused - try { - this.writer = KnxNetProtocol.createWriter(); - switch (datagram.service_type) { - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - // append the CEMI service type if this is a tunneling request... - cemitype = keyText("MESSAGECODES", datagram.cemi.msgcode); - break; - } - const packet = this.writer.KNXNetHeader(datagram); - const buf = packet.buffer; - const svctype = keyText("SERVICE_TYPE", datagram.service_type); // TODO: unused - const descr = datagramDesc(datagram); - KnxLog.get().trace( - "(%s): Sending %s ==> %j", - this.compositeState(), - descr, - datagram - ); - this.socket.send( - buf, - 0, - buf.length, - this.remoteEndpoint.port, - this.remoteEndpoint.addr.toString(), - (err) => { - KnxLog.get().trace( - "(%s): UDP sent %s: %s %s", - this.compositeState(), - err ? err.toString() : "OK", - descr, - buf.toString("hex") - ); - if (typeof callback === "function") callback(err); - } - ); - } catch (e) { - KnxLog.get().warn(e); - if (typeof callback === "function") callback(e); - } - } - - write( - grpaddr: string, - value: any, - dptid?: number, - callback?: () => void - ): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn("You must supply both grpaddr and value!"); - return; - } - try { - // outbound request onto the state machine - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request( - serviceType, - function (datagram: Datagram) { - populateAPDU(value, datagram.cemi.apdu, dptid); - datagram.cemi.dest_addr = grpaddr; - }, - callback - ); - } catch (e) { - KnxLog.get().warn(e); - } - } - - respond(grpaddr: string, value: any, dptid: number): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn("You must supply both grpaddr and value!"); - return; - } - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function (datagram: Datagram) { - populateAPDU(value, datagram.cemi.apdu, dptid); - // this is a READ request - datagram.cemi.apdu.apci = "GroupValue_Response"; - datagram.cemi.dest_addr = grpaddr; - return datagram; - }); - } - - writeRaw( - grpaddr: string, - value: Buffer, - bitlength?: number, - callback?: () => void - ): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn("You must supply both grpaddr and value!"); - return; - } - if (!Buffer.isBuffer(value)) { - KnxLog.get().warn("Value must be a buffer!"); - return; - } - // outbound request onto the state machine - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request( - serviceType, - function (datagram: Datagram) { - datagram.cemi.apdu.data = value; - datagram.cemi.apdu.bitlength = bitlength - ? bitlength - : value.byteLength * 8; - datagram.cemi.dest_addr = grpaddr; - }, - callback - ); - } - - // send a READ request to the bus - // you can pass a callback function which gets bound to the RESPONSE datagram event - read(grpaddr: string, callback: (src: any, data: any) => void): void { - if (typeof callback == "function") { - // when the response arrives: - const responseEvent = "GroupValue_Response_" + grpaddr; - KnxLog.get().trace("Binding connection to " + responseEvent); - const binding = (src: any, data: any) => { - // unbind the event handler - this.off(responseEvent, binding); - // fire the callback - callback(src, data); - }; - // prepare for the response - this.on(responseEvent, binding); - // clean up after 3 seconds just in case no one answers the read request - setTimeout(() => this.off(responseEvent, binding), 3000); - } - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; - this.Request(serviceType, function (datagram: Datagram) { - // this is a READ request - datagram.cemi.apdu.apci = "GroupValue_Read"; - datagram.cemi.dest_addr = grpaddr; - return datagram; - }); - } - - Disconnect(cb: () => void): void { - if (this.state === "connecting") { - KnxLog.get().debug("Disconnecting directly"); - this.transition("uninitialized"); - if (cb) { - cb(); - } - return; - } - - KnxLog.get().debug("waiting for Idle-State"); - this.onIdle(() => { - KnxLog.get().trace("In Idle-State"); - - this.on("disconnected", () => { - KnxLog.get().debug("Disconnected from KNX"); - if (cb) { - cb(); - } - }); - - KnxLog.get().debug("Disconnecting from KNX"); - this.transition("disconnecting"); - }); - - // machina.js removeAllListeners equivalent: - // this.off(); - } - - onIdle(cb: () => void): void { - if (this.state === "idle") { - KnxLog.get().trace("Connection is already Idle"); - cb(); - } else { - this.on("transition", function (data: any) { - if (data.toState === "idle") { - KnxLog.get().trace("Connection just transitioned to Idle"); - cb(); - } - }); - } - } + send(datagram: Datagram, callback: (err?: Error) => void): void { + let cemitype: string // TODO: set, but unused + try { + this.writer = KnxNetProtocol.createWriter() + switch (datagram.service_type) { + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + // append the CEMI service type if this is a tunneling request... + cemitype = keyText('MESSAGECODES', datagram.cemi.msgcode) + break + } + const packet = this.writer.KNXNetHeader(datagram) + const buf = packet.buffer + const svctype = keyText('SERVICE_TYPE', datagram.service_type) // TODO: unused + const descr = datagramDesc(datagram) + KnxLog.get().trace( + '(%s): Sending %s ==> %j', + this.compositeState(), + descr, + datagram, + ) + this.socket.send( + buf, + 0, + buf.length, + this.remoteEndpoint.port, + this.remoteEndpoint.addr.toString(), + (err) => { + KnxLog.get().trace( + '(%s): UDP sent %s: %s %s', + this.compositeState(), + err ? err.toString() : 'OK', + descr, + buf.toString('hex'), + ) + if (typeof callback === 'function') callback(err) + }, + ) + } catch (e) { + KnxLog.get().warn(e) + if (typeof callback === 'function') callback(e) + } + } + + write( + grpaddr: string, + value: any, + dptid?: number, + callback?: () => void, + ): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn('You must supply both grpaddr and value!') + return + } + try { + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request( + serviceType, + (datagram: Datagram) => { + populateAPDU(value, datagram.cemi.apdu, dptid) + datagram.cemi.dest_addr = grpaddr + }, + callback, + ) + } catch (e) { + KnxLog.get().warn(e) + } + } + + respond(grpaddr: string, value: any, dptid: number): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn('You must supply both grpaddr and value!') + return + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request(serviceType, function (datagram: Datagram) { + populateAPDU(value, datagram.cemi.apdu, dptid) + // this is a READ request + datagram.cemi.apdu.apci = 'GroupValue_Response' + datagram.cemi.dest_addr = grpaddr + return datagram + }) + } + + writeRaw( + grpaddr: string, + value: Buffer, + bitlength?: number, + callback?: () => void, + ): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn('You must supply both grpaddr and value!') + return + } + if (!Buffer.isBuffer(value)) { + KnxLog.get().warn('Value must be a buffer!') + return + } + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request( + serviceType, + function (datagram: Datagram) { + datagram.cemi.apdu.data = value + datagram.cemi.apdu.bitlength = bitlength || value.byteLength * 8 + datagram.cemi.dest_addr = grpaddr + }, + callback, + ) + } + + // send a READ request to the bus + // you can pass a callback function which gets bound to the RESPONSE datagram event + read(grpaddr: string, callback: (src: any, data: any) => void): void { + if (typeof callback === 'function') { + // when the response arrives: + const responseEvent = `GroupValue_Response_${grpaddr}` + KnxLog.get().trace(`Binding connection to ${responseEvent}`) + const binding = (src: any, data: any) => { + // unbind the event handler + this.off(responseEvent, binding) + // fire the callback + callback(src, data) + } + // prepare for the response + this.on(responseEvent, binding) + // clean up after 3 seconds just in case no one answers the read request + setTimeout(() => this.off(responseEvent, binding), 3000) + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request(serviceType, function (datagram: Datagram) { + // this is a READ request + datagram.cemi.apdu.apci = 'GroupValue_Read' + datagram.cemi.dest_addr = grpaddr + return datagram + }) + } + + Disconnect(cb: () => void): void { + if (this.state === 'connecting') { + KnxLog.get().debug('Disconnecting directly') + this.transition('uninitialized') + if (cb) { + cb() + } + return + } + + KnxLog.get().debug('waiting for Idle-State') + this.onIdle(() => { + KnxLog.get().trace('In Idle-State') + + this.on('disconnected', () => { + KnxLog.get().debug('Disconnected from KNX') + if (cb) { + cb() + } + }) + + KnxLog.get().debug('Disconnecting from KNX') + this.transition('disconnecting') + }) + + // machina.js removeAllListeners equivalent: + // this.off(); + } + + onIdle(cb: () => void): void { + if (this.state === 'idle') { + KnxLog.get().trace('Connection is already Idle') + cb() + } else { + this.on('transition', function (data: any) { + if (data.toState === 'idle') { + KnxLog.get().trace('Connection just transitioned to Idle') + cb() + } + }) + } + } } // return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) const datagramDesc = (dg: Datagram): string => { - let blurb = keyText("SERVICE_TYPE", dg.service_type); - if ( - dg.service_type == KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || - dg.service_type == KnxConstants.SERVICE_TYPE.ROUTING_INDICATION - ) { - blurb += "_" + keyText("MESSAGECODES", dg.cemi.msgcode); - } - return blurb; -}; + let blurb = keyText('SERVICE_TYPE', dg.service_type) + if ( + dg.service_type === KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || + dg.service_type === KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + ) { + blurb += `_${keyText('MESSAGECODES', dg.cemi.msgcode)}` + } + return blurb +} // add the control udp local endpoint. UPDATE: not needed apparnently? const AddHPAI = (datagram: Datagram): void => { - datagram.hpai = { - protocol_type: 1, // UDP - //tunnel_endpoint: this.localAddress + ":" + this.control.address().port - tunnel_endpoint: "0.0.0.0:0", - }; -}; + datagram.hpai = { + protocol_type: 1, // UDP + // tunnel_endpoint: this.localAddress + ":" + this.control.address().port + tunnel_endpoint: '0.0.0.0:0', + } +} // add the tunneling udp local endpoint UPDATE: not needed apparently? const AddTunn = (datagram: Datagram): void => { - datagram.tunn = { - protocol_type: 1, // UDP - tunnel_endpoint: "0.0.0.0:0", - //tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port - }; -}; - -export default KnxFSMConnection; + datagram.tunn = { + protocol_type: 1, // UDP + tunnel_endpoint: '0.0.0.0:0', + // tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port + } +} + +export default KnxFSMConnection diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index 298eb70..25a70b0 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -1,61 +1,70 @@ -import * as util from 'util'; -import * as dgram from 'dgram'; -import KnxLog from './KnxLog'; -import KnxFSMConnection from './FSM'; +import * as util from 'util' +import * as dgram from 'dgram' +import KnxLog from './KnxLog' +import type { KnxFSMConnection } from './FSM' function IpRoutingConnection(instance: KnxFSMConnection): KnxFSMConnection { - const log = KnxLog.get(); + const log = KnxLog.get() - instance.BindSocket = function (cb: (socket: dgram.Socket) => void): dgram.Socket { - const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true }); - udpSocket.on('listening', () => { - log.debug( - util.format( - 'IpRoutingConnection %s:%d, adding membership for %s', - instance.localAddress, - udpSocket.address().port, - this.remoteEndpoint.addr - ) - ); - try { - this.socket.addMembership( - this.remoteEndpoint.addr, - instance.localAddress - ); - } catch (err) { - log.warn('IPRouting connection: cannot add membership (%s)', err); - } - }); - // ROUTING multicast connections need to bind to the default port, 3671 - udpSocket.bind(3671, () => cb && cb(udpSocket)); - return udpSocket; - }; + instance.BindSocket = (cb: (socket: dgram.Socket) => void) => { + const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true }) + udpSocket.on('listening', () => { + log.debug( + util.format( + 'IpRoutingConnection %s:%d, adding membership for %s', + instance.localAddress, + udpSocket.address().port, + this.remoteEndpoint.addr, + ), + ) + try { + this.socket.addMembership( + this.remoteEndpoint.addr, + instance.localAddress, + ) + } catch (err) { + log.warn( + 'IPRouting connection: cannot add membership (%s)', + err, + ) + } + }) + // ROUTING multicast connections need to bind to the default port, 3671 + udpSocket.bind(3671, () => cb && cb(udpSocket)) + return udpSocket + } - // - /// Start the connection - /// - instance.Connect = function (): KnxFSMConnection { - this.localAddress = this.getLocalAddress(); - this.socket = this.BindSocket((socket: dgram.Socket) => { - socket.on('error', (errmsg: string) => - log.debug(util.format('Socket error: %j', errmsg)) - ); - socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo, callback: () => void) => { - log.debug( - 'Inbound multicast message from ' + - rinfo.address + - ': ' + - msg.toString('hex') - ); - this.onUdpSocketMessage(msg, rinfo, callback); - }); - // start connection sequence - this.transition('connecting'); - }); - return this; - }; + // + /// Start the connection + /// + instance.Connect = () => { + this.localAddress = this.getLocalAddress() + this.socket = this.BindSocket((socket: dgram.Socket) => { + socket.on('error', (errmsg: string) => + log.debug(util.format('Socket error: %j', errmsg)), + ) + socket.on( + 'message', + ( + msg: Buffer, + rinfo: dgram.RemoteInfo, + callback: () => void, + ) => { + log.debug( + `Inbound multicast message from ${ + rinfo.address + }: ${msg.toString('hex')}`, + ) + this.onUdpSocketMessage(msg, rinfo, callback) + }, + ) + // start connection sequence + this.transition('connecting') + }) + return this + } - return instance; + return instance } -export default IpRoutingConnection; \ No newline at end of file +export default IpRoutingConnection diff --git a/src/IpTunnelingConnection.ts b/src/IpTunnelingConnection.ts index 45e2801..a36228d 100644 --- a/src/IpTunnelingConnection.ts +++ b/src/IpTunnelingConnection.ts @@ -1,46 +1,48 @@ -import dgram from "dgram"; -import KnxLog from "./KnxLog"; -import KnxFSMConnection from "./FSM"; +import dgram from 'dgram' +import KnxLog from './KnxLog' +import type { KnxFSMConnection } from './FSM' -function IpTunnelingConnection(instance: KnxFSMConnection): KnxFSMConnection { - const log = KnxLog.get(); +function IpTunnelingConnection(instance: KnxFSMConnection) { + const log = KnxLog.get() - instance.BindSocket = function ( - cb: (socket: dgram.Socket) => void - ): dgram.Socket { - const udpSocket = dgram.createSocket("udp4"); - udpSocket.bind(() => { - log.debug( - "IpTunnelingConnection.BindSocket %s:%d", - instance.localAddress, - udpSocket.address().port - ); - cb && cb(udpSocket); - }); - return udpSocket; - }; + instance.BindSocket = (cb: (socket: dgram.Socket) => void) => { + const udpSocket = dgram.createSocket('udp4') + udpSocket.bind(() => { + log.debug( + 'IpTunnelingConnection.BindSocket %s:%d', + instance.localAddress, + udpSocket.address().port, + ) + if (cb) cb(udpSocket) + }) + return udpSocket + } - instance.Connect = function (): KnxFSMConnection { - this.localAddress = this.getLocalAddress(); - // create the socket - this.socket = this.BindSocket((socket: dgram.Socket) => { - socket.on("error", (errmsg: string) => - log.debug("Socket error: %j", errmsg) - ); - socket.on( - "message", - (msg: Buffer, rinfo: dgram.RemoteInfo, callback: () => void) => { - log.debug("Inbound message: %s", msg.toString("hex")); - this.onUdpSocketMessage(msg, rinfo, callback); - } - ); - // start connection sequence - this.transition("connecting"); - }); - return this; - }; + instance.Connect = () => { + this.localAddress = this.getLocalAddress() + // create the socket + this.socket = this.BindSocket((socket: dgram.Socket) => { + socket.on('error', (errmsg: string) => + log.debug('Socket error: %j', errmsg), + ) + socket.on( + 'message', + ( + msg: Buffer, + rinfo: dgram.RemoteInfo, + callback: () => void, + ) => { + log.debug('Inbound message: %s', msg.toString('hex')) + this.onUdpSocketMessage(msg, rinfo, callback) + }, + ) + // start connection sequence + this.transition('connecting') + }) + return this + } - return instance; + return instance } -export default IpTunnelingConnection; +export default IpTunnelingConnection diff --git a/src/KnxConstants.ts b/src/KnxConstants.ts index 3d895b7..b1adbf1 100644 --- a/src/KnxConstants.ts +++ b/src/KnxConstants.ts @@ -3,122 +3,117 @@ * (C) 2016-2018 Elias Karakoulakis */ -import KnxLog from './KnxLog'; - +import KnxLog from './KnxLog' const SERVICE_TYPE = { - SEARCH_REQUEST: 0x0201, - SEARCH_RESPONSE: 0x0202, - DESCRIPTION_REQUEST: 0x0203, - DESCRIPTION_RESPONSE: 0x0204, - CONNECT_REQUEST: 0x0205, - CONNECT_RESPONSE: 0x0206, - CONNECTIONSTATE_REQUEST: 0x0207, - CONNECTIONSTATE_RESPONSE: 0x0208, - DISCONNECT_REQUEST: 0x0209, - DISCONNECT_RESPONSE: 0x020a, - DEVICE_CONFIGURATION_REQUEST: 0x0310, - DEVICE_CONFIGURATION_ACK: 0x0311, - TUNNELING_REQUEST: 0x0420, - TUNNELING_ACK: 0x0421, - ROUTING_INDICATION: 0x0530, - ROUTING_LOST_MESSAGE: 0x0531, - UNKNOWN: -1, -}; + SEARCH_REQUEST: 0x0201, + SEARCH_RESPONSE: 0x0202, + DESCRIPTION_REQUEST: 0x0203, + DESCRIPTION_RESPONSE: 0x0204, + CONNECT_REQUEST: 0x0205, + CONNECT_RESPONSE: 0x0206, + CONNECTIONSTATE_REQUEST: 0x0207, + CONNECTIONSTATE_RESPONSE: 0x0208, + DISCONNECT_REQUEST: 0x0209, + DISCONNECT_RESPONSE: 0x020a, + DEVICE_CONFIGURATION_REQUEST: 0x0310, + DEVICE_CONFIGURATION_ACK: 0x0311, + TUNNELING_REQUEST: 0x0420, + TUNNELING_ACK: 0x0421, + ROUTING_INDICATION: 0x0530, + ROUTING_LOST_MESSAGE: 0x0531, + UNKNOWN: -1, +} const CONNECTION_TYPE = { - DEVICE_MGMT_CONNECTION: 0x03, - TUNNEL_CONNECTION: 0x04, - REMOTE_LOGGING_CONNECTION: 0x06, - REMOTE_CONFIGURATION_CONNECTION: 0x07, - OBJECT_SERVER_CONNECTION: 0x08, -}; + DEVICE_MGMT_CONNECTION: 0x03, + TUNNEL_CONNECTION: 0x04, + REMOTE_LOGGING_CONNECTION: 0x06, + REMOTE_CONFIGURATION_CONNECTION: 0x07, + OBJECT_SERVER_CONNECTION: 0x08, +} const PROTOCOL_TYPE = { - IPV4_UDP: 0x01, - IPV4_TCP: 0x02, -}; + IPV4_UDP: 0x01, + IPV4_TCP: 0x02, +} const KNX_LAYER = { - LINK_LAYER: 0x02, - RAW_LAYER: 0x04, - BUSMONITOR_LAYER: 0x80, -}; + LINK_LAYER: 0x02, + RAW_LAYER: 0x04, + BUSMONITOR_LAYER: 0x80, +} const FRAMETYPE = { - EXTENDED: 0x00, - STANDARD: 0x01, -}; + EXTENDED: 0x00, + STANDARD: 0x01, +} const RESPONSECODE = { - NO_ERROR: 0x00, - E_HOST_PROTOCOL_TYPE: 0x01, - E_VERSION_NOT_SUPPORTED: 0x02, - E_SEQUENCE_NUMBER: 0x04, - E_CONNSTATE_LOST: 0x15, - E_CONNECTION_ID: 0x21, - E_CONNECTION_TYPE: 0x22, - E_CONNECTION_OPTION: 0x23, - E_NO_MORE_CONNECTIONS: 0x24, - E_DATA_CONNECTION: 0x26, - E_KNX_CONNECTION: 0x27, - E_TUNNELING_LAYER: 0x29, -}; + NO_ERROR: 0x00, + E_HOST_PROTOCOL_TYPE: 0x01, + E_VERSION_NOT_SUPPORTED: 0x02, + E_SEQUENCE_NUMBER: 0x04, + E_CONNSTATE_LOST: 0x15, + E_CONNECTION_ID: 0x21, + E_CONNECTION_TYPE: 0x22, + E_CONNECTION_OPTION: 0x23, + E_NO_MORE_CONNECTIONS: 0x24, + E_DATA_CONNECTION: 0x26, + E_KNX_CONNECTION: 0x27, + E_TUNNELING_LAYER: 0x29, +} const MESSAGECODES = { - 'L_Raw.req': 0x10, - 'L_Data.req': 0x11, - 'L_Poll_Data.req': 0x13, - 'L_Poll_Data.con': 0x25, - 'L_Data.ind': 0x29, - 'L_Busmon.ind': 0x2b, - 'L_Raw.ind': 0x2d, - 'L_Data.con': 0x2e, - 'L_Raw.con': 0x2f, - 'ETS.Dummy1': 0xc1, -}; + 'L_Raw.req': 0x10, + 'L_Data.req': 0x11, + 'L_Poll_Data.req': 0x13, + 'L_Poll_Data.con': 0x25, + 'L_Data.ind': 0x29, + 'L_Busmon.ind': 0x2b, + 'L_Raw.ind': 0x2d, + 'L_Data.con': 0x2e, + 'L_Raw.con': 0x2f, + 'ETS.Dummy1': 0xc1, +} const APCICODES: string[] = [ - 'GroupValue_Read', - 'GroupValue_Response', - 'GroupValue_Write', - 'PhysicalAddress_Write', - 'PhysicalAddress_Read', - 'PhysicalAddress_Response', - 'ADC_Read', - 'ADC_Response', - 'Memory_Read', - 'Memory_Response', - 'Memory_Write', - 'UserMemory', - 'DeviceDescriptor_Read', - 'DeviceDescriptor_Response', - 'Restart', - 'OTHER', -]; + 'GroupValue_Read', + 'GroupValue_Response', + 'GroupValue_Write', + 'PhysicalAddress_Write', + 'PhysicalAddress_Read', + 'PhysicalAddress_Response', + 'ADC_Read', + 'ADC_Response', + 'Memory_Read', + 'Memory_Response', + 'Memory_Write', + 'UserMemory', + 'DeviceDescriptor_Read', + 'DeviceDescriptor_Response', + 'Restart', + 'OTHER', +] const KnxConstants = { - SERVICE_TYPE, - CONNECTION_TYPE, - PROTOCOL_TYPE, - KNX_LAYER, - FRAMETYPE, - RESPONSECODE, - MESSAGECODES, -}; + SERVICE_TYPE, + CONNECTION_TYPE, + PROTOCOL_TYPE, + KNX_LAYER, + FRAMETYPE, + RESPONSECODE, + MESSAGECODES, +} /* TODO helper function to print enum keys */ const keyText = (mapref: string | object, value: number): string => { - const map = typeof mapref === 'string' ? KnxConstants[mapref] : mapref; + const map = typeof mapref === 'string' ? KnxConstants[mapref] : mapref - if (typeof map !== 'object') throw 'Unknown map: ' + mapref; - for (const [key, v] of Object.entries(map)) if (v == value) return key; + if (typeof map !== 'object') throw Error(`Unknown map: ${mapref}`) + for (const [key, v] of Object.entries(map)) if (v === value) return key - KnxLog.get().trace('not found: %j', value); -}; + KnxLog.get().trace('not found: %j', value) +} -export { - KnxConstants, - APCICODES, - keyText, -}; \ No newline at end of file +export { KnxConstants, APCICODES, keyText } diff --git a/src/KnxLog.ts b/src/KnxLog.ts index d070b7c..4e2dcd4 100644 --- a/src/KnxLog.ts +++ b/src/KnxLog.ts @@ -1,32 +1,39 @@ -import util from 'util'; -import factory, { Logger, LogLevel } from 'log-driver'; - +import util from 'util' +import factory, { Logger, LogLevel } from 'log-driver' export interface KnxLogger { - get: (options?: KnxLogOptions) => Logger; + get: (options?: KnxLogOptions) => Logger } -let logger: Logger; +let logger: Logger export interface KnxLogOptions { - debug?: boolean; - loglevel?: LogLevel; + debug?: boolean + loglevel?: LogLevel } const create = (options: KnxLogOptions): Logger => { - const level: LogLevel = - (options && (options.debug ? 'debug' : options.loglevel)) || 'info'; - return factory({ - level, - format(lvl: LogLevel, msg: string, ...a: any[]) { - const ts = new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''); - return a.length - ? util.format('[%s] %s ' + msg, lvl, ts, ...a) - : util.format('[%s] %s %s', lvl, ts, msg); - }, - }); -}; + const level: LogLevel = + (options && (options.debug ? 'debug' : options.loglevel)) || 'info' + return factory({ + level, + format(lvl: LogLevel, msg: string, ...a: any[]) { + const ts = new Date() + .toISOString() + .replace(/T/, ' ') + .replace(/Z$/, '') + return a.length + ? util.format(`[%s] %s ${msg}`, lvl, ts, ...a) + : util.format('[%s] %s %s', lvl, ts, msg) + }, + }) +} -const KnxLog: KnxLogger = { get: (options: KnxLogOptions): Logger => logger || (logger = create(options)) }; +const KnxLog: KnxLogger = { + get: (options: KnxLogOptions): Logger => { + if (!logger) logger = create(options) + return logger + }, +} -export default KnxLog; \ No newline at end of file +export default KnxLog diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index 17c4587..0ec1513 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -3,56 +3,61 @@ * (C) 2016-2018 Elias Karakoulakis */ -import util from "util"; -import ipaddr from "ipaddr.js"; +import util from 'util' +import ipaddr from 'ipaddr.js' import { Parser } from 'binary-parser' -import BinaryProtocol from "binary-protocol"; -import * as KnxAddress from "./Address"; -import { APCICODES, keyText, KnxConstants } from "./KnxConstants"; -import KnxLog from "./KnxLog"; -import { Datagram } from "./FSM"; - -export interface KnxProtocol extends BinaryProtocol { - lengths: { [key: string]: (value: any) => number }; - twoLevelAddressing: boolean; - debug: boolean; - apduStruct: Parser; +import BinaryProtocol from 'binary-protocol' +import * as KnxAddress from './Address' +import { APCICODES, keyText, KnxConstants } from './KnxConstants' +import KnxLog from './KnxLog' +import type { Datagram } from './FSM' + +export interface KnxProtocolI extends BinaryProtocol { + lengths: { [key: string]: (value: any) => number } + twoLevelAddressing: boolean + debug: boolean + apduStruct: Parser } -const KnxProtocol: KnxProtocol = new BinaryProtocol() as KnxProtocol; +const KnxProtocol: KnxProtocolI = new BinaryProtocol() as KnxProtocolI // defaults -KnxProtocol.twoLevelAddressing = false; -KnxProtocol.lengths = {}; // TODO: Can this be a local variable, do we need to expose it? +KnxProtocol.twoLevelAddressing = false +KnxProtocol.lengths = {} // TODO: Can this be a local variable, do we need to expose it? // helper function: what is the byte length of an object? const knxlen = (objectName: string, context: any) => { - const lf = KnxProtocol.lengths[objectName]; - return typeof lf === "function" ? lf(context) : lf; -}; - -KnxProtocol.define("IPv4Endpoint", { - read(propertyName: string) { - this.pushStack({ addr: null, port: null }) - .raw("addr", 4) - .UInt16BE("port") - .tap((hdr) => { - hdr.addr = ipaddr.fromByteArray(hdr.addr); - }) - .popStack(propertyName, (data) => data.addr.toString() + ":" + data.port); - }, - write(value: string) { - if (!value) throw "cannot write null value for IPv4Endpoint"; - - if (typeof value !== "string" || !value.match(/\d*\.\d*\.\d*\.\d*:\d*/)) - throw "Invalid IPv4 endpoint, please set a string as 'ip.add.re.ss:port'"; - - const [addr, port] = value.split(":"); - this.raw(Buffer.from(ipaddr.parse(addr).toByteArray()), 4); - this.UInt16BE(port); - }, -}); - -KnxProtocol.lengths["IPv4Endpoint"] = (value: string) => (value ? 6 : 0); + const lf = KnxProtocol.lengths[objectName] + return typeof lf === 'function' ? lf(context) : lf +} + +KnxProtocol.define('IPv4Endpoint', { + read(propertyName: string) { + this.pushStack({ addr: null, port: null }) + .raw('addr', 4) + .UInt16BE('port') + .tap((hdr) => { + hdr.addr = ipaddr.fromByteArray(hdr.addr) + }) + .popStack( + propertyName, + (data) => `${data.addr.toString()}:${data.port}`, + ) + }, + write(value: string) { + if (!value) throw Error('cannot write null value for IPv4Endpoint') + + if (typeof value !== 'string' || !value.match(/\d*\.\d*\.\d*\.\d*:\d*/)) + throw Error( + "Invalid IPv4 endpoint, please set a string as 'ip.add.re.ss:port'", + ) + + const [addr, port] = value.split(':') + this.raw(Buffer.from(ipaddr.parse(addr).toByteArray()), 4) + this.UInt16BE(port) + }, +}) + +KnxProtocol.lengths['IPv4Endpoint'] = (value: string) => (value ? 6 : 0) /* CRI: connection request/response */ // creq[22] = 0x04; /* structure len (4 bytes) */ @@ -60,101 +65,108 @@ KnxProtocol.lengths["IPv4Endpoint"] = (value: string) => (value ? 6 : 0); // creq[24] = 0x02; /* KNX Layer (Tunnel Link Layer) */ // creq[25] = 0x00; /* Reserved */ // ==> 4 bytes -KnxProtocol.define("CRI", { - read(propertyName: string) { - this.pushStack({ - header_length: 0, - connection_type: null, - knx_layer: null, - unused: null, - }) // - .UInt8("header_length") - .UInt8("connection_type") - .UInt8("knx_layer") - .UInt8("unused") - .tap((hdr: Datagram["cri"]) => { - switch (hdr.connection_type) { - case KnxConstants.CONNECTION_TYPE.DEVICE_MGMT_CONNECTION: - break; // TODO - case KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION: - break; // TODO - default: - throw "Unsupported connection type: " + hdr.connection_type; - } - }) - .popStack(propertyName, (data) => { - if (KnxProtocol.debug) - KnxLog.get().debug("read CRI: " + JSON.stringify(data)); - // pop the interim value off the stack and insert the real value into `propertyName` - return data; - }); - }, - write(value: Datagram["cri"]) { - if (!value) - return KnxLog.get().warn("CRI: cannot write null value for CRI"); - this.UInt8(0x04) // length - .UInt8(value.connection_type) - .UInt8(value.knx_layer) - .UInt8(value.unused); - }, -}); -KnxProtocol.lengths["CRI"] = (value: Datagram["cri"]) => (value ? 4 : 0); +KnxProtocol.define('CRI', { + read(propertyName: string) { + this.pushStack({ + header_length: 0, + connection_type: null, + knx_layer: null, + unused: null, + }) // + .UInt8('header_length') + .UInt8('connection_type') + .UInt8('knx_layer') + .UInt8('unused') + .tap((hdr: Datagram['cri']) => { + switch (hdr.connection_type) { + case KnxConstants.CONNECTION_TYPE.DEVICE_MGMT_CONNECTION: + break // TODO + case KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION: + break // TODO + default: + throw Error( + `Unsupported connection type: ${hdr.connection_type}`, + ) + } + }) + .popStack(propertyName, (data) => { + if (KnxProtocol.debug) + KnxLog.get().debug(`read CRI: ${JSON.stringify(data)}`) + // pop the interim value off the stack and insert the real value into `propertyName` + return data + }) + }, + write(value: Datagram['cri']) { + if (!value) + return KnxLog.get().warn('CRI: cannot write null value for CRI') + this.UInt8(0x04) // length + .UInt8(value.connection_type) + .UInt8(value.knx_layer) + .UInt8(value.unused) + }, +}) +KnxProtocol.lengths['CRI'] = (value: Datagram['cri']) => (value ? 4 : 0) // connection state response/request -KnxProtocol.define("ConnState", { - read(propertyName: string) { - this.pushStack({ channel_id: null, status: null }) - .UInt8("channel_id") - .UInt8("status") - .popStack(propertyName, (data: any) => { - if (KnxProtocol.debug) KnxLog.get().trace("read ConnState: %j", data); - return data; - }); - }, - write(value: Datagram["connstate"]) { - if (!value) - return KnxLog.get().error("cannot write null value for ConnState"); - this.UInt8(value.channel_id).UInt8(value.status); - }, -}); -KnxProtocol.lengths["ConnState"] = (value: Datagram["connstate"]) => (value ? 2 : 0); +KnxProtocol.define('ConnState', { + read(propertyName: string) { + this.pushStack({ channel_id: null, status: null }) + .UInt8('channel_id') + .UInt8('status') + .popStack(propertyName, (data: any) => { + if (KnxProtocol.debug) + KnxLog.get().trace('read ConnState: %j', data) + return data + }) + }, + write(value: Datagram['connstate']) { + if (!value) + return KnxLog.get().error('cannot write null value for ConnState') + this.UInt8(value.channel_id).UInt8(value.status) + }, +}) +KnxProtocol.lengths['ConnState'] = (value: Datagram['connstate']) => + value ? 2 : 0 // connection state response/request -KnxProtocol.define("TunnState", { - read(propertyName: string) { - this.pushStack({ - header_length: null, - channel_id: null, - seqnum: null, - rsvd: null, - }) - .UInt8("header_length") - .UInt8("channel_id") - .UInt8("seqnum") - .UInt8("rsvd") - .tap((hdr: any) => { - if (KnxProtocol.debug) KnxLog.get().trace("reading TunnState: %j", hdr); - switch (hdr.status) { - case 0x00: - break; - //default: throw "Connection State status: " + hdr.status; - } - }) - .popStack(propertyName, (data) => data); - }, - write(value: Datagram["tunnstate"]) { - if (!value) - return KnxLog.get().error( - "TunnState: cannot write null value for TunnState" - ); - if (KnxProtocol.debug) KnxLog.get().trace("writing TunnState: %j", value); - this.UInt8(0x04) - .UInt8(value.channel_id) - .UInt8(value.seqnum) - .UInt8(value.rsvd); - }, -}); -KnxProtocol.lengths["TunnState"] = (value: Datagram["tunnstate"]) => (value ? 4 : 0); +KnxProtocol.define('TunnState', { + read(propertyName: string) { + this.pushStack({ + header_length: null, + channel_id: null, + seqnum: null, + rsvd: null, + }) + .UInt8('header_length') + .UInt8('channel_id') + .UInt8('seqnum') + .UInt8('rsvd') + .tap((hdr: any) => { + if (KnxProtocol.debug) + KnxLog.get().trace('reading TunnState: %j', hdr) + switch (hdr.status) { + case 0x00: + break + // default: throw "Connection State status: " + hdr.status; + } + }) + .popStack(propertyName, (data) => data) + }, + write(value: Datagram['tunnstate']) { + if (!value) + return KnxLog.get().error( + 'TunnState: cannot write null value for TunnState', + ) + if (KnxProtocol.debug) + KnxLog.get().trace('writing TunnState: %j', value) + this.UInt8(0x04) + .UInt8(value.channel_id) + .UInt8(value.seqnum) + .UInt8(value.rsvd) + }, +}) +KnxProtocol.lengths['TunnState'] = (value: Datagram['tunnstate']) => + value ? 4 : 0 /* Connection HPAI */ // creq[6] = /* Host Protocol Address Information (HPAI) Lenght */ @@ -169,54 +181,54 @@ KnxProtocol.lengths["TunnState"] = (value: Datagram["tunnstate"]) => (value ? 4 // creq[16-19] = /* IPv4 address */ // creq[20-21] = /* IPv4 local port number for TUNNELING requests */ // ==> 8 bytes -KnxProtocol.define("HPAI", { - read(propertyName: string) { - this.pushStack({ - header_length: 8, - protocol_type: null, - tunnel_endpoint: null, - }) - .UInt8("header_length") - .UInt8("protocol_type") - .IPv4Endpoint("tunnel_endpoint") - .tap(function (hdr: Datagram["hpai"]) { - if (this.buffer.length < hdr.header_length) { - if (KnxProtocol.debug) - KnxLog.get().trace( - "%d %d %d", - this.buffer.length, - this.offset, - hdr.header_length - ); - throw "Incomplete KNXNet HPAI header"; - } - if (KnxProtocol.debug) { - KnxLog.get().trace( - "read HPAI: %j, proto = %s", - hdr, - keyText("PROTOCOL_TYPE", hdr.protocol_type) - ); - } - switch (hdr.protocol_type) { - case KnxConstants.PROTOCOL_TYPE.IPV4_TCP: - throw "TCP is not supported"; - default: - } - }) - .popStack(propertyName, (data) => data); - }, - write(value: Datagram["hpai"]) { - if (!value) - return KnxLog.get().error("HPAI: cannot write null value for HPAI"); - - this.UInt8(0x08) // length: 8 bytes - .UInt8(value.protocol_type) - .IPv4Endpoint(value.tunnel_endpoint); - }, -}); -KnxProtocol.lengths["HPAI"] = (value: Datagram["hpai"]) => { - return value ? 8 : 0; -}; +KnxProtocol.define('HPAI', { + read(propertyName: string) { + this.pushStack({ + header_length: 8, + protocol_type: null, + tunnel_endpoint: null, + }) + .UInt8('header_length') + .UInt8('protocol_type') + .IPv4Endpoint('tunnel_endpoint') + .tap(function (hdr: Datagram['hpai']) { + if (this.buffer.length < hdr.header_length) { + if (KnxProtocol.debug) + KnxLog.get().trace( + '%d %d %d', + this.buffer.length, + this.offset, + hdr.header_length, + ) + throw Error('Incomplete KNXNet HPAI header') + } + if (KnxProtocol.debug) { + KnxLog.get().trace( + 'read HPAI: %j, proto = %s', + hdr, + keyText('PROTOCOL_TYPE', hdr.protocol_type), + ) + } + switch (hdr.protocol_type) { + case KnxConstants.PROTOCOL_TYPE.IPV4_TCP: + throw Error('TCP is not supported') + default: + } + }) + .popStack(propertyName, (data) => data) + }, + write(value: Datagram['hpai']) { + if (!value) + return KnxLog.get().error('HPAI: cannot write null value for HPAI') + + this.UInt8(0x08) // length: 8 bytes + .UInt8(value.protocol_type) + .IPv4Endpoint(value.tunnel_endpoint) + }, +}) +KnxProtocol.lengths['HPAI'] = (value: Datagram['hpai']) => { + return value ? 8 : 0 +} /* ==================== APCI ====================== */ // @@ -340,7 +352,7 @@ Control Field 2 // + B Y T E 1 || B Y T E 2 // +-----------------------------------------------------------------------++-------------.... -//Total number of APCI control bits can be either 4 or 10. The second byte bit structure is as follows: +// Total number of APCI control bits can be either 4 or 10. The second byte bit structure is as follows: // Bit 1 Bit 2 Bit 3 Bit 4 Bit 5 Bit 6 Bit 7 Bit 8 Bit 1 Bit 2 // +--------+--------+--------+--------+--------+--------+--------+--------++--------+----.... @@ -353,354 +365,366 @@ Control Field 2 // control field const ctrlStruct = new Parser() - // byte 1 - .bit1("frameType") - .bit1("reserved") - .bit1("repeat") - .bit1("broadcast") - .bit2("priority") - .bit1("acknowledge") - .bit1("confirm") - // byte 2 - .bit1("destAddrType") - .bit3("hopCount") - .bit4("extendedFrame"); + // byte 1 + .bit1('frameType') + .bit1('reserved') + .bit1('repeat') + .bit1('broadcast') + .bit2('priority') + .bit1('acknowledge') + .bit1('confirm') + // byte 2 + .bit1('destAddrType') + .bit3('hopCount') + .bit4('extendedFrame') // APDU: 2 bytes, tcpi = 6 bits, apci = 4 bits, remaining 6 bits = data (when length=1) -KnxProtocol.apduStruct = new Parser().bit6("tpci").bit4("apci").bit6("data"); - -KnxProtocol.define("APDU", { - read(propertyName: string) { - this.pushStack({ - apdu_length: null, - apdu_raw: null, - tpci: null, - apci: null, - data: null, - }) - .UInt8("apdu_length") - .tap(function (hdr: Datagram["cemi"]["apdu"]) { - //if (KnxProtocol.debug) KnxLog.get().trace('--- parsing extra %d apdu bytes', hdr.apdu_length+1); - this.raw("apdu_raw", hdr.apdu_length + 1); - }) - .tap((hdr: Datagram["cemi"]["apdu"]) => { - // Parse the APDU. tcpi/apci bits split across byte boundary. - // Typical example of protocol designed by committee. - const apdu = KnxProtocol.apduStruct.parse(hdr.apdu_raw); - hdr.tpci = apdu.tpci; - hdr.apci = APCICODES[apdu.apci]; - // APDU data should ALWAYS be a buffer, even for 1-bit payloads - hdr.data = - hdr.apdu_length > 1 - ? hdr.apdu_raw.slice(2) - : Buffer.from([apdu.data]); - if (KnxProtocol.debug) - KnxLog.get().trace(" unmarshalled APDU: %j", hdr); - }) - .popStack(propertyName, (data) => data); - }, - write(value: Datagram["cemi"]["apdu"]) { - if (!value) throw "cannot write null APDU value"; - const total_length = knxlen("APDU", value); - //if (KnxProtocol.debug) KnxLog.get().trace('APDU.write: \t%j (total %d bytes)', value, total_length); - if (APCICODES.indexOf(value.apci) == -1) - return KnxLog.get().error("invalid APCI code: %j", value); - if (total_length < 3) - throw util.format("APDU is too small (%d bytes)", total_length); - if (total_length > 17) - throw util.format("APDU is too big (%d bytes)", total_length); - // camel designed by committee: total length MIGHT or MIGHT NOT include the payload - // APDU length (1 byte) + TPCI/APCI: 6+4 bits + DATA: 6 bits (2 bytes) - // OR: APDU length (1 byte) + TPCI/APCI: 6+4(+6 unused) bits (2bytes) + DATA: (1 to 14 bytes)) - this.UInt8(total_length - 2); - let word = - value.tpci * 0x400 + APCICODES.indexOf(value.apci) * 0x40; - // - if (total_length == 3) { - // payload embedded in the last 6 bits - word += parseInt( - isFinite(value.data) && typeof value.data !== "object" - ? value.data - : value.data[0] - ); - this.UInt16BE(word); - } else { - this.UInt16BE(word); - // payload follows TPCI+APCI word - // KnxLog.get().trace('~~~%s, %j, %d', typeof value.data, value.data, total_length); - this.raw(Buffer.from(value.data, total_length - 3)); - } - }, -}); +KnxProtocol.apduStruct = new Parser().bit6('tpci').bit4('apci').bit6('data') + +KnxProtocol.define('APDU', { + read(propertyName: string) { + this.pushStack({ + apdu_length: null, + apdu_raw: null, + tpci: null, + apci: null, + data: null, + }) + .UInt8('apdu_length') + .tap(function (hdr: Datagram['cemi']['apdu']) { + // if (KnxProtocol.debug) KnxLog.get().trace('--- parsing extra %d apdu bytes', hdr.apdu_length+1); + this.raw('apdu_raw', hdr.apdu_length + 1) + }) + .tap((hdr: Datagram['cemi']['apdu']) => { + // Parse the APDU. tcpi/apci bits split across byte boundary. + // Typical example of protocol designed by committee. + const apdu = KnxProtocol.apduStruct.parse(hdr.apdu_raw) + hdr.tpci = apdu.tpci + hdr.apci = APCICODES[apdu.apci] + // APDU data should ALWAYS be a buffer, even for 1-bit payloads + hdr.data = + hdr.apdu_length > 1 + ? hdr.apdu_raw.slice(2) + : Buffer.from([apdu.data]) + if (KnxProtocol.debug) + KnxLog.get().trace(' unmarshalled APDU: %j', hdr) + }) + .popStack(propertyName, (data) => data) + }, + write(value: Datagram['cemi']['apdu']) { + if (!value) throw Error('cannot write null APDU value') + const total_length = knxlen('APDU', value) + // if (KnxProtocol.debug) KnxLog.get().trace('APDU.write: \t%j (total %d bytes)', value, total_length); + if (APCICODES.indexOf(value.apci) === -1) + return KnxLog.get().error('invalid APCI code: %j', value) + if (total_length < 3) + throw Error( + util.format('APDU is too small (%d bytes)', total_length), + ) + if (total_length > 17) + throw Error(util.format('APDU is too big (%d bytes)', total_length)) + // camel designed by committee: total length MIGHT or MIGHT NOT include the payload + // APDU length (1 byte) + TPCI/APCI: 6+4 bits + DATA: 6 bits (2 bytes) + // OR: APDU length (1 byte) + TPCI/APCI: 6+4(+6 unused) bits (2bytes) + DATA: (1 to 14 bytes)) + this.UInt8(total_length - 2) + let word = value.tpci * 0x400 + APCICODES.indexOf(value.apci) * 0x40 + // + if (total_length === 3) { + // payload embedded in the last 6 bits + word += parseInt( + isFinite(value.data) && typeof value.data !== 'object' + ? value.data + : value.data[0], + ) + this.UInt16BE(word) + } else { + this.UInt16BE(word) + // payload follows TPCI+APCI word + // KnxLog.get().trace('~~~%s, %j, %d', typeof value.data, value.data, total_length); + this.raw(Buffer.from(value.data, total_length - 3)) + } + }, +}) /* APDU length is truly chaotic: header and data can be interleaved (but not always!), so that apdu_length=1 means _2_ bytes following the apdu_length */ -KnxProtocol.lengths["APDU"] = (value) => { - if (!value) return 0; - // if we have the APDU bitlength, usually by the DPT, then simply use it - if (value.bitlength || (value.data && value.data.bitlength)) { - const bitlen = value.bitlength || value.data.bitlength; - // KNX spec states that up to 6 bits of payload must fit into the TPCI - // if payload larger than 6 bits, than append it AFTER the TPCI - return 3 + (bitlen > 6 ? Math.ceil(bitlen / 8) : 0); - } - // not all requests carry a value; eg read requests - if (!value.data) value.data = 0; - if (value.data.length) { - if (value.data.length < 1) throw "APDU value is empty"; - if (value.data.length > 14) throw "APDU value too big, must be <= 14 bytes"; - if (value.data.length == 1) { - const v = value.data[0]; - if (!isNaN(parseFloat(v)) && isFinite(v) && v >= 0 && v <= 63) { - // apdu_length + tpci/apci/6-bit integer == 1+2 bytes - return 3; - } - } - return 3 + value.data.length; - } else { - if ( - !isNaN(parseFloat(value.data)) && - isFinite(value.data) && - value.data >= 0 && - value.data <= 63 - ) { - return 3; - } else { - KnxLog.get().warn( - "Fix your code - APDU data payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes), got: %j (%s)", - value.data, - typeof value.data - ); - throw "APDU payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes)"; - } - } -}; - -KnxProtocol.define("CEMI", { - read(propertyName: string) { - this.pushStack({ - msgcode: 0, - addinfo_length: -1, - ctrl: null, - src_addr: null, - dest_addr: null, - apdu: null, - }) - .UInt8("msgcode") - .UInt8("addinfo_length") - .tap(function (hdr: Datagram["cemi"]) { - if (hdr.addinfo_length !== 0) { - this.raw("addinfo", hdr.addinfo_length); - } - }) - .raw("ctrl", 2) - .raw("src_addr", 2) - .raw("dest_addr", 2) - .tap(function (hdr: Datagram["cemi"]) { - // parse 16bit control field - hdr.ctrl = ctrlStruct.parse(hdr.ctrl as unknown as Buffer); - // KNX source addresses are always physical - hdr.src_addr = KnxAddress.toString( - hdr.src_addr, - KnxAddress.TYPE.PHYSICAL - ); - hdr.dest_addr = KnxAddress.toString( - hdr.dest_addr, - hdr.ctrl.destAddrType - ); - switch (hdr.msgcode) { - case KnxConstants.MESSAGECODES["L_Data.req"]: - case KnxConstants.MESSAGECODES["L_Data.ind"]: - case KnxConstants.MESSAGECODES["L_Data.con"]: { - this.APDU("apdu"); - if (KnxProtocol.debug) - KnxLog.get().trace("--- unmarshalled APDU ==> %j", hdr.apdu); - } - } - }) - .popStack(propertyName, (data) => data); - }, - write(value: Datagram["cemi"]) { - if (!value) throw "cannot write null CEMI value"; - if (KnxProtocol.debug) KnxLog.get().trace("CEMI.write: \n\t%j", value); - if (value.ctrl === null) throw "no Control Field supplied"; - const ctrlField1 = - value.ctrl.frameType * 0x80 + - value.ctrl.reserved * 0x40 + - value.ctrl.repeat * 0x20 + - value.ctrl.broadcast * 0x10 + - value.ctrl.priority * 0x04 + - value.ctrl.acknowledge * 0x02 + - value.ctrl.confirm; - const ctrlField2 = - value.ctrl.destAddrType * 0x80 + - value.ctrl.hopCount * 0x10 + - value.ctrl.extendedFrame; - this.UInt8(value.msgcode) - .UInt8(value.addinfo_length) - .UInt8(ctrlField1) - .UInt8(ctrlField2) - .raw(KnxAddress.parse(value.src_addr, KnxAddress.TYPE.PHYSICAL), 2) - .raw(KnxAddress.parse(value.dest_addr, value.ctrl.destAddrType), 2); - // only need to marshal an APDU if this is a - // L_Data.* (requet/indication/confirmation) - switch (value.msgcode) { - case KnxConstants.MESSAGECODES["L_Data.req"]: - case KnxConstants.MESSAGECODES["L_Data.ind"]: - case KnxConstants.MESSAGECODES["L_Data.con"]: { - if (value.apdu === null) throw "no APDU supplied"; - this.APDU(value.apdu); - } - } - }, -}); - -KnxProtocol.lengths["CEMI"] = (value: Datagram["cemi"]) => { - if (!value) return 0; - const apdu_length = knxlen("APDU", value.apdu); - if (KnxProtocol.debug) - KnxLog.get().trace("knxlen of cemi: %j == %d", value, 8 + apdu_length); - return 8 + apdu_length; -}; - -KnxProtocol.define("KNXNetHeader", { - read(propertyName: string) { - this.pushStack({ - header_length: 0, - protocol_version: -1, - service_type: -1, - total_length: 0, - }) - .UInt8("header_length") - .UInt8("protocol_version") - .UInt16BE("service_type") - .UInt16BE("total_length") - .tap(function (hdr: Datagram) { - if (KnxProtocol.debug) KnxLog.get().trace("read KNXNetHeader :%j", hdr); - if (this.buffer.length + hdr.header_length < this.total_length) - throw util.format( - "Incomplete KNXNet packet: got %d bytes (expected %d)", - this.buffer.length + hdr.header_length, - this.total_length - ); - switch (hdr.service_type) { - // case SERVICE_TYPE.SEARCH_REQUEST: - case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: { - this.HPAI("hpai").HPAI("tunn").CRI("cri"); - break; - } - case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: - case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: - case KnxConstants.SERVICE_TYPE.DISCONNECT_RESPONSE: { - this.ConnState("connstate"); - if (hdr.total_length > 8) this.HPAI("hpai"); - if (hdr.total_length > 16) this.CRI("cri"); - break; - } - case KnxConstants.SERVICE_TYPE.DESCRIPTION_RESPONSE: { - this.raw("value", hdr.total_length); - break; - } - // most common case: - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - this.TunnState("tunnstate"); - this.CEMI("cemi"); - break; - case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - this.TunnState("tunnstate"); - break; - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - this.CEMI("cemi"); - break; - default: { - KnxLog.get().warn( - "read KNXNetHeader: unhandled serviceType = %s", - keyText("SERVICE_TYPE", hdr.service_type) - ); - } - } - }) - .popStack(propertyName, (data) => { - if (KnxProtocol.debug) - KnxLog.get().trace(JSON.stringify(data, null, 4)); - return data; - }); - }, - write(value: Datagram) { - if (!value) throw "cannot write null KNXNetHeader value"; - value.total_length = knxlen("KNXNetHeader", value); - if (KnxProtocol.debug) KnxLog.get().trace("writing KnxHeader:", value); - this.UInt8(6) // header length (6 bytes constant) - .UInt8(0x10) // protocol version 1.0 - .UInt16BE(value.service_type) - .UInt16BE(value.total_length); - switch (value.service_type) { - //case SERVICE_TYPE.SEARCH_REQUEST: - case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: { - if (value.hpai) this.HPAI(value.hpai); - if (value.tunn) this.HPAI(value.tunn); - if (value.cri) this.CRI(value.cri); - break; - } - case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: - case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: { - if (value.connstate) this.ConnState(value.connstate); - if (value.hpai) this.HPAI(value.hpai); - if (value.cri) this.CRI(value.cri); - break; - } - // most common case: - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: { - if (value.tunnstate) this.TunnState(value.tunnstate); - if (value.cemi) this.CEMI(value.cemi); - break; - } - // case KnxConstants.SERVICE_TYPE.DESCRIPTION_RESPONSE: { - default: { - throw util.format( - "write KNXNetHeader: unhandled serviceType = %s (%j)", - keyText("SERVICE_TYPE", value.service_type), - value - ); - } - } - }, -}); -KnxProtocol.lengths["KNXNetHeader"] = (value: Datagram) => { - if (!value) throw "Must supply a valid KNXNetHeader value"; - switch (value.service_type) { - //case SERVICE_TYPE.SEARCH_REQUEST: - case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: - return ( - 6 + - knxlen("HPAI", value.hpai) + - knxlen("HPAI", value.tunn) + - knxlen("CRI", value.cri) - ); - case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: - case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: - return ( - 6 + - knxlen("ConnState", value.connstate) + - knxlen("HPAI", value.hpai) + - knxlen("CRI", value.cri) - ); - case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - return ( - 6 + knxlen("TunnState", value.tunnstate) + knxlen("CEMI", value.cemi) - ); - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - return 6 + knxlen("CEMI", value.cemi); - } -}; - -export default KnxProtocol; +KnxProtocol.lengths['APDU'] = (value) => { + if (!value) return 0 + // if we have the APDU bitlength, usually by the DPT, then simply use it + if (value.bitlength || (value.data && value.data.bitlength)) { + const bitlen = value.bitlength || value.data.bitlength + // KNX spec states that up to 6 bits of payload must fit into the TPCI + // if payload larger than 6 bits, than append it AFTER the TPCI + return 3 + (bitlen > 6 ? Math.ceil(bitlen / 8) : 0) + } + // not all requests carry a value; eg read requests + if (!value.data) value.data = 0 + if (value.data.length) { + if (value.data.length < 1) throw Error('APDU value is empty') + if (value.data.length > 14) + throw Error('APDU value too big, must be <= 14 bytes') + if (value.data.length === 1) { + const v = value.data[0] + if (!isNaN(parseFloat(v)) && isFinite(v) && v >= 0 && v <= 63) { + // apdu_length + tpci/apci/6-bit integer == 1+2 bytes + return 3 + } + } + return 3 + value.data.length + } + if ( + !isNaN(parseFloat(value.data)) && + isFinite(value.data) && + value.data >= 0 && + value.data <= 63 + ) { + return 3 + } + KnxLog.get().warn( + 'Fix your code - APDU data payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes), got: %j (%s)', + value.data, + typeof value.data, + ) + throw Error( + 'APDU payload must be a 6-bit int or an Array/Buffer (1 to 14 bytes)', + ) +} + +KnxProtocol.define('CEMI', { + read(propertyName: string) { + this.pushStack({ + msgcode: 0, + addinfo_length: -1, + ctrl: null, + src_addr: null, + dest_addr: null, + apdu: null, + }) + .UInt8('msgcode') + .UInt8('addinfo_length') + .tap(function (hdr: Datagram['cemi']) { + if (hdr.addinfo_length !== 0) { + this.raw('addinfo', hdr.addinfo_length) + } + }) + .raw('ctrl', 2) + .raw('src_addr', 2) + .raw('dest_addr', 2) + .tap(function (hdr: Datagram['cemi']) { + // parse 16bit control field + hdr.ctrl = ctrlStruct.parse(hdr.ctrl as unknown as Buffer) + // KNX source addresses are always physical + hdr.src_addr = KnxAddress.toString( + hdr.src_addr, + KnxAddress.TYPE.PHYSICAL, + ) + hdr.dest_addr = KnxAddress.toString( + hdr.dest_addr, + hdr.ctrl.destAddrType, + ) + switch (hdr.msgcode) { + case KnxConstants.MESSAGECODES['L_Data.req']: + case KnxConstants.MESSAGECODES['L_Data.ind']: + case KnxConstants.MESSAGECODES['L_Data.con']: { + this.APDU('apdu') + if (KnxProtocol.debug) + KnxLog.get().trace( + '--- unmarshalled APDU ==> %j', + hdr.apdu, + ) + } + } + }) + .popStack(propertyName, (data) => data) + }, + write(value: Datagram['cemi']) { + if (!value) throw Error('cannot write null CEMI value') + if (KnxProtocol.debug) KnxLog.get().trace('CEMI.write: \n\t%j', value) + if (value.ctrl === null) throw Error('no Control Field supplied') + const ctrlField1 = + value.ctrl.frameType * 0x80 + + value.ctrl.reserved * 0x40 + + value.ctrl.repeat * 0x20 + + value.ctrl.broadcast * 0x10 + + value.ctrl.priority * 0x04 + + value.ctrl.acknowledge * 0x02 + + value.ctrl.confirm + const ctrlField2 = + value.ctrl.destAddrType * 0x80 + + value.ctrl.hopCount * 0x10 + + value.ctrl.extendedFrame + this.UInt8(value.msgcode) + .UInt8(value.addinfo_length) + .UInt8(ctrlField1) + .UInt8(ctrlField2) + .raw(KnxAddress.parse(value.src_addr, KnxAddress.TYPE.PHYSICAL), 2) + .raw(KnxAddress.parse(value.dest_addr, value.ctrl.destAddrType), 2) + // only need to marshal an APDU if this is a + // L_Data.* (requet/indication/confirmation) + switch (value.msgcode) { + case KnxConstants.MESSAGECODES['L_Data.req']: + case KnxConstants.MESSAGECODES['L_Data.ind']: + case KnxConstants.MESSAGECODES['L_Data.con']: { + if (value.apdu === null) throw Error('no APDU supplied') + this.APDU(value.apdu) + } + } + }, +}) + +KnxProtocol.lengths['CEMI'] = (value: Datagram['cemi']) => { + if (!value) return 0 + const apdu_length = knxlen('APDU', value.apdu) + if (KnxProtocol.debug) + KnxLog.get().trace('knxlen of cemi: %j == %d', value, 8 + apdu_length) + return 8 + apdu_length +} + +KnxProtocol.define('KNXNetHeader', { + read(propertyName: string) { + this.pushStack({ + header_length: 0, + protocol_version: -1, + service_type: -1, + total_length: 0, + }) + .UInt8('header_length') + .UInt8('protocol_version') + .UInt16BE('service_type') + .UInt16BE('total_length') + .tap(function (hdr: Datagram) { + if (KnxProtocol.debug) + KnxLog.get().trace('read KNXNetHeader :%j', hdr) + if (this.buffer.length + hdr.header_length < this.total_length) + throw Error( + util.format( + 'Incomplete KNXNet packet: got %d bytes (expected %d)', + this.buffer.length + hdr.header_length, + this.total_length, + ), + ) + switch (hdr.service_type) { + // case SERVICE_TYPE.SEARCH_REQUEST: + case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: { + this.HPAI('hpai').HPAI('tunn').CRI('cri') + break + } + case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: + case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: + case KnxConstants.SERVICE_TYPE.DISCONNECT_RESPONSE: { + this.ConnState('connstate') + if (hdr.total_length > 8) this.HPAI('hpai') + if (hdr.total_length > 16) this.CRI('cri') + break + } + case KnxConstants.SERVICE_TYPE.DESCRIPTION_RESPONSE: { + this.raw('value', hdr.total_length) + break + } + // most common case: + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + this.TunnState('tunnstate') + this.CEMI('cemi') + break + case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: + this.TunnState('tunnstate') + break + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + this.CEMI('cemi') + break + default: { + KnxLog.get().warn( + 'read KNXNetHeader: unhandled serviceType = %s', + keyText('SERVICE_TYPE', hdr.service_type), + ) + } + } + }) + .popStack(propertyName, (data) => { + if (KnxProtocol.debug) + KnxLog.get().trace(JSON.stringify(data, null, 4)) + return data + }) + }, + write(value: Datagram) { + if (!value) throw Error('cannot write null KNXNetHeader value') + value.total_length = knxlen('KNXNetHeader', value) + if (KnxProtocol.debug) KnxLog.get().trace('writing KnxHeader:', value) + this.UInt8(6) // header length (6 bytes constant) + .UInt8(0x10) // protocol version 1.0 + .UInt16BE(value.service_type) + .UInt16BE(value.total_length) + switch (value.service_type) { + // case SERVICE_TYPE.SEARCH_REQUEST: + case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: { + if (value.hpai) this.HPAI(value.hpai) + if (value.tunn) this.HPAI(value.tunn) + if (value.cri) this.CRI(value.cri) + break + } + case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: + case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: { + if (value.connstate) this.ConnState(value.connstate) + if (value.hpai) this.HPAI(value.hpai) + if (value.cri) this.CRI(value.cri) + break + } + // most common case: + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: { + if (value.tunnstate) this.TunnState(value.tunnstate) + if (value.cemi) this.CEMI(value.cemi) + break + } + // case KnxConstants.SERVICE_TYPE.DESCRIPTION_RESPONSE: { + default: { + throw Error( + util.format( + 'write KNXNetHeader: unhandled serviceType = %s (%j)', + keyText('SERVICE_TYPE', value.service_type), + value, + ), + ) + } + } + }, +}) +KnxProtocol.lengths['KNXNetHeader'] = (value: Datagram) => { + if (!value) throw Error('Must supply a valid KNXNetHeader value') + switch (value.service_type) { + // case SERVICE_TYPE.SEARCH_REQUEST: + case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: + return ( + 6 + + knxlen('HPAI', value.hpai) + + knxlen('HPAI', value.tunn) + + knxlen('CRI', value.cri) + ) + case KnxConstants.SERVICE_TYPE.CONNECT_RESPONSE: + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: + case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: + return ( + 6 + + knxlen('ConnState', value.connstate) + + knxlen('HPAI', value.hpai) + + knxlen('CRI', value.cri) + ) + case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + return ( + 6 + + knxlen('TunnState', value.tunnstate) + + knxlen('CEMI', value.cemi) + ) + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + return 6 + knxlen('CEMI', value.cemi) + } +} + +export default KnxProtocol diff --git a/src/devices/BinarySwitch.ts b/src/devices/BinarySwitch.ts index 1c25df0..19851de 100644 --- a/src/devices/BinarySwitch.ts +++ b/src/devices/BinarySwitch.ts @@ -1,56 +1,63 @@ -import Datapoint from '../Datapoint'; -import KnxLog, { Logger } from '../KnxLog'; +import { Logger } from 'log-driver' +import Datapoint from '../Datapoint' +import KnxLog from '../KnxLog' export default class BinarySwitch { - private control_ga: string; - private status_ga: string; - private conn: any; - private control: Datapoint; - private status: Datapoint; - private log: Logger; - - constructor(options: { ga: string, status_ga: string }, conn: any) { - if (!options || !options.ga) throw 'must supply at least { ga }!'; - - this.control_ga = options.ga; - this.status_ga = options.status_ga; - if (conn) this.bind(conn); - this.log = KnxLog.get(); - } - - bind(conn: any) { - if (!conn) this.log.warn('must supply a valid KNX connection to bind to'); - this.conn = conn; - this.control = new Datapoint({ ga: this.control_ga }, conn); - if (this.status_ga) - this.status = new Datapoint({ ga: this.status_ga }, conn); - } - - // EventEmitter proxy for status ga (if its set), otherwise proxy control ga - on(...args: any[]) { - const tgt = this.status_ga ? this.status : this.control; - try { - tgt.on.apply(tgt, args); - } catch (err) { - this.log.error(err); - } - } - - switchOn() { - if (!this.conn) - this.log.warn('must supply a valid KNX connection to bind to'); - this.control.write(1); - } - - switchOff() { - if (!this.conn) - this.log.warn('must supply a valid KNX connection to bind to'); - this.control.write(0); - } - - write(v: any) { - if (!this.conn) - this.log.warn('must supply a valid KNX connection to bind to'); - this.control.write(v); - } -} \ No newline at end of file + private control_ga: string + + private status_ga: string + + private conn: any + + private control: Datapoint + + private status: Datapoint + + private log: Logger + + constructor(options: { ga: string; status_ga: string }, conn: any) { + if (!options || !options.ga) throw Error('must supply at least { ga }!') + + this.control_ga = options.ga + this.status_ga = options.status_ga + if (conn) this.bind(conn) + this.log = KnxLog.get() + } + + bind(conn: any) { + if (!conn) + this.log.warn('must supply a valid KNX connection to bind to') + this.conn = conn + this.control = new Datapoint({ ga: this.control_ga }, conn) + if (this.status_ga) + this.status = new Datapoint({ ga: this.status_ga }, conn) + } + + // EventEmitter proxy for status ga (if its set), otherwise proxy control ga + on(...args: any[]) { + const tgt = this.status_ga ? this.status : this.control + try { + tgt.on.call(tgt, ...args) + } catch (err) { + this.log.error(err) + } + } + + switchOn() { + if (!this.conn) + this.log.warn('must supply a valid KNX connection to bind to') + this.control.write(1) + } + + switchOff() { + if (!this.conn) + this.log.warn('must supply a valid KNX connection to bind to') + this.control.write(0) + } + + write(v: any) { + if (!this.conn) + this.log.warn('must supply a valid KNX connection to bind to') + this.control.write(v) + } +} diff --git a/src/devices/index.ts b/src/devices/index.ts index 56cbf1c..5f3898e 100644 --- a/src/devices/index.ts +++ b/src/devices/index.ts @@ -1,5 +1,5 @@ -import BinarySwitch from './BinarySwitch'; +import BinarySwitch from './BinarySwitch' export default { - BinarySwitch, -}; \ No newline at end of file + BinarySwitch, +} diff --git a/src/dptlib/dpt1.ts b/src/dptlib/dpt1.ts index 448a4ad..5984f35 100644 --- a/src/dptlib/dpt1.ts +++ b/src/dptlib/dpt1.ts @@ -3,233 +3,233 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() const custom_truthiness = (value: string | boolean): boolean => { - const f = parseFloat(value as string); - return !isNaN(f) && isFinite(f) - ? // numeric values (in native and string form) are truthy if NOT zero - f !== 0.0 - : // non-numeric value truthiness is Boolean true or the string 'true'. - value === true || value === "true"; -}; + const f = parseFloat(value as string) + return !isNaN(f) && isFinite(f) + ? // numeric values (in native and string form) are truthy if NOT zero + f !== 0.0 + : // non-numeric value truthiness is Boolean true or the string 'true'. + value === true || value === 'true' +} const config: DatapointConfig = { - id: "DPT1", - formatAPDU: (value) => Buffer.from([custom_truthiness(value) ? 1 : 0]), - - fromBuffer: (buf) => { - if (buf.length !== 1) { - log.warn( - "DPT1.fromBuffer: buf should be 1 byte (got %d bytes)", - buf.length - ); - - return null; - } - - return buf[0] !== 0; - }, - - // DPT basetype info hash - basetype: { - bitlength: 1, - valuetype: "basic", - desc: "1-bit value", - }, - - // DPT subtypes info hash - subtypes: { - // 1.001 on/off - "001": { - use: "G", - name: "DPT_Switch", - desc: "switch", - enc: { 0: "Off", 1: "On" }, - }, - - // 1.002 boolean - "002": { - use: "G", - name: "DPT_Bool", - desc: "bool", - enc: { 0: "false", 1: "true" }, - }, - - // 1.003 enable - "003": { - use: "G", - name: "DPT_Enable", - desc: "enable", - enc: { 0: "disable", 1: "enable" }, - }, - - // 1.004 ramp - "004": { - use: "FB", - name: "DPT_Ramp", - desc: "ramp", - enc: { 0: "No ramp", 1: "Ramp" }, - }, - - // 1.005 alarm - "005": { - use: "FB", - name: "DPT_Alarm", - desc: "alarm", - enc: { 0: "No alarm", 1: "Alarm" }, - }, - - // 1.006 binary value - "006": { - use: "FB", - name: "DPT_BinaryValue", - desc: "binary value", - enc: { 0: "Low", 1: "High" }, - }, - - // 1.007 step - "007": { - use: "FB", - name: "DPT_Step", - desc: "step", - enc: { 0: "Decrease", 1: "Increase" }, - }, - - // 1.008 up/down - "008": { - use: "G", - name: "DPT_UpDown", - desc: "up/down", - enc: { 0: "Up", 1: "Down" }, - }, - - // 1.009 open/close - "009": { - use: "G", - name: "DPT_OpenClose", - desc: "open/close", - enc: { 0: "Open", 1: "Close" }, - }, - - // 1.010 start/stop - "010": { - use: "G", - name: "DPT_Start", - desc: "start/stop", - enc: { 0: "Stop", 1: "Start" }, - }, - - // 1.011 state - "011": { - use: "FB", - name: "DPT_State", - desc: "state", - enc: { 0: "Inactive", 1: "Active" }, - }, - - // 1.012 invert - "012": { - use: "FB", - name: "DPT_Invert", - desc: "invert", - enc: { 0: "Not inverted", 1: "inverted" }, - }, - - // 1.013 dim send style - "013": { - use: "FB", - name: "DPT_DimSendStyle", - desc: "dim send style", - enc: { 0: "Start/stop", 1: "Cyclically" }, - }, - - // 1.014 input source - "014": { - use: "FB", - name: "DPT_InputSource", - desc: "input source", - enc: { 0: "Fixed", 1: "Calculated" }, - }, - - // 1.015 reset - "015": { - use: "G", - name: "DPT_Reset", - desc: "reset", - enc: { 0: "no action(dummy)", 1: "reset command(trigger)" }, - }, - - // 1.016 acknowledge - "016": { - use: "G", - name: "DPT_Ack", - desc: "ack", - enc: { 0: "no action(dummy)", 1: "acknowledge command(trigger)" }, - }, - - // 1.017 trigger - "017": { - use: "G", - name: "DPT_Trigger", - desc: "trigger", - enc: { 0: "trigger", 1: "trigger" }, - }, - - // 1.018 occupied - "018": { - use: "G", - name: "DPT_Occupancy", - desc: "occupancy", - enc: { 0: "not occupied", 1: "occupied" }, - }, - - // 1.019 open window or door - "019": { - use: "G", - name: "DPT_WindowDoor", - desc: "open window/door", - enc: { 0: "closed", 1: "open" }, - }, - - // 1.021 and/or - "021": { - use: "FB", - name: "DPT_LogicalFunction", - desc: "and/or", - enc: { 0: "logical function OR", 1: "logical function AND" }, - }, - - // 1.022 scene A/B - "022": { - use: "FB", - name: "DPT_Scene_AB", - desc: "scene A/B", - enc: { 0: "scene A", 1: "scene B" }, - }, - - // 1.023 shutter/blinds mode - "023": { - use: "FB", - name: "DPT_ShutterBlinds_Mode", - desc: "shutter/blinds mode", - enc: { - 0: "only move Up/Down mode (shutter)", - 1: "move Up/Down + StepStop mode (blind)", - }, - }, - - // 1.100 cooling/heating ---FIXME--- - 100: { - use: "???", - name: "DPT_Heat/Cool", - desc: "heat/cool", - enc: { 0: "???", 1: "???" }, - }, - }, -}; - -export default config; + id: 'DPT1', + formatAPDU: (value) => Buffer.from([custom_truthiness(value) ? 1 : 0]), + + fromBuffer: (buf) => { + if (buf.length !== 1) { + log.warn( + 'DPT1.fromBuffer: buf should be 1 byte (got %d bytes)', + buf.length, + ) + + return null + } + + return buf[0] !== 0 + }, + + // DPT basetype info hash + basetype: { + bitlength: 1, + valuetype: 'basic', + desc: '1-bit value', + }, + + // DPT subtypes info hash + subtypes: { + // 1.001 on/off + '001': { + use: 'G', + name: 'DPT_Switch', + desc: 'switch', + enc: { 0: 'Off', 1: 'On' }, + }, + + // 1.002 boolean + '002': { + use: 'G', + name: 'DPT_Bool', + desc: 'bool', + enc: { 0: 'false', 1: 'true' }, + }, + + // 1.003 enable + '003': { + use: 'G', + name: 'DPT_Enable', + desc: 'enable', + enc: { 0: 'disable', 1: 'enable' }, + }, + + // 1.004 ramp + '004': { + use: 'FB', + name: 'DPT_Ramp', + desc: 'ramp', + enc: { 0: 'No ramp', 1: 'Ramp' }, + }, + + // 1.005 alarm + '005': { + use: 'FB', + name: 'DPT_Alarm', + desc: 'alarm', + enc: { 0: 'No alarm', 1: 'Alarm' }, + }, + + // 1.006 binary value + '006': { + use: 'FB', + name: 'DPT_BinaryValue', + desc: 'binary value', + enc: { 0: 'Low', 1: 'High' }, + }, + + // 1.007 step + '007': { + use: 'FB', + name: 'DPT_Step', + desc: 'step', + enc: { 0: 'Decrease', 1: 'Increase' }, + }, + + // 1.008 up/down + '008': { + use: 'G', + name: 'DPT_UpDown', + desc: 'up/down', + enc: { 0: 'Up', 1: 'Down' }, + }, + + // 1.009 open/close + '009': { + use: 'G', + name: 'DPT_OpenClose', + desc: 'open/close', + enc: { 0: 'Open', 1: 'Close' }, + }, + + // 1.010 start/stop + '010': { + use: 'G', + name: 'DPT_Start', + desc: 'start/stop', + enc: { 0: 'Stop', 1: 'Start' }, + }, + + // 1.011 state + '011': { + use: 'FB', + name: 'DPT_State', + desc: 'state', + enc: { 0: 'Inactive', 1: 'Active' }, + }, + + // 1.012 invert + '012': { + use: 'FB', + name: 'DPT_Invert', + desc: 'invert', + enc: { 0: 'Not inverted', 1: 'inverted' }, + }, + + // 1.013 dim send style + '013': { + use: 'FB', + name: 'DPT_DimSendStyle', + desc: 'dim send style', + enc: { 0: 'Start/stop', 1: 'Cyclically' }, + }, + + // 1.014 input source + '014': { + use: 'FB', + name: 'DPT_InputSource', + desc: 'input source', + enc: { 0: 'Fixed', 1: 'Calculated' }, + }, + + // 1.015 reset + '015': { + use: 'G', + name: 'DPT_Reset', + desc: 'reset', + enc: { 0: 'no action(dummy)', 1: 'reset command(trigger)' }, + }, + + // 1.016 acknowledge + '016': { + use: 'G', + name: 'DPT_Ack', + desc: 'ack', + enc: { 0: 'no action(dummy)', 1: 'acknowledge command(trigger)' }, + }, + + // 1.017 trigger + '017': { + use: 'G', + name: 'DPT_Trigger', + desc: 'trigger', + enc: { 0: 'trigger', 1: 'trigger' }, + }, + + // 1.018 occupied + '018': { + use: 'G', + name: 'DPT_Occupancy', + desc: 'occupancy', + enc: { 0: 'not occupied', 1: 'occupied' }, + }, + + // 1.019 open window or door + '019': { + use: 'G', + name: 'DPT_WindowDoor', + desc: 'open window/door', + enc: { 0: 'closed', 1: 'open' }, + }, + + // 1.021 and/or + '021': { + use: 'FB', + name: 'DPT_LogicalFunction', + desc: 'and/or', + enc: { 0: 'logical function OR', 1: 'logical function AND' }, + }, + + // 1.022 scene A/B + '022': { + use: 'FB', + name: 'DPT_Scene_AB', + desc: 'scene A/B', + enc: { 0: 'scene A', 1: 'scene B' }, + }, + + // 1.023 shutter/blinds mode + '023': { + use: 'FB', + name: 'DPT_ShutterBlinds_Mode', + desc: 'shutter/blinds mode', + enc: { + 0: 'only move Up/Down mode (shutter)', + 1: 'move Up/Down + StepStop mode (blind)', + }, + }, + + // 1.100 cooling/heating ---FIXME--- + 100: { + use: '???', + name: 'DPT_Heat/Cool', + desc: 'heat/cool', + enc: { 0: '???', 1: '???' }, + }, + }, +} + +export default config diff --git a/src/dptlib/dpt10.ts b/src/dptlib/dpt10.ts index 1ca0c96..96b4a40 100644 --- a/src/dptlib/dpt10.ts +++ b/src/dptlib/dpt10.ts @@ -1,105 +1,115 @@ +/* eslint-disable no-fallthrough */ /** * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT10.*: time (3 bytes) // -const dowTimeRegexp = /((\d)\/)?(\d{1,2}):(\d{1,2}):(\d{1,2})/; +const dowTimeRegexp = /((\d)\/)?(\d{1,2}):(\d{1,2}):(\d{1,2})/ // DPTFrame to parse a DPT10 frame. // Always 8-bit aligned. const config: DatapointConfig = { - id: "DPT10", - formatAPDU: (value) => { - let dow: number, hour: number, minute: number, second: number; - // day of week. NOTE: JS Sunday = 0 - switch (typeof value) { - case "string": - // try to parse - const match = dowTimeRegexp.exec(value); - if (match) { - const currentDoW = ((new Date().getDay() - 7) % 7) + 7; - dow = match[2] != undefined ? parseInt(match[2]) : currentDoW; - hour = parseInt(match[3]); - minute = parseInt(match[4]); - second = parseInt(match[5]); - } else { - log.warn("DPT10: invalid time format (%s)", value); - } - break; - case "object": - if (value.constructor.name != "Date") { - log.warn("Must supply a Date or String for DPT10 time"); - break; - } - case "number": - value = new Date(value); - default: - dow = ((value.getDay() - 7) % 7) + 7; - hour = value.getHours(); - minute = value.getMinutes(); - second = value.getSeconds(); - } + id: 'DPT10', + formatAPDU: (value) => { + let dow: number + let hour: number + let minute: number + let second: number + // day of week. NOTE: JS Sunday = 0 + switch (typeof value) { + case 'string': + { + // try to parse + const match = dowTimeRegexp.exec(value) + if (match) { + const currentDoW = ((new Date().getDay() - 7) % 7) + 7 + dow = + match[2] !== undefined + ? parseInt(match[2]) + : currentDoW + hour = parseInt(match[3]) + minute = parseInt(match[4]) + second = parseInt(match[5]) + } else { + log.warn('DPT10: invalid time format (%s)', value) + } + } + break + case 'object': + if (value.constructor.name !== 'Date') { + log.warn('Must supply a Date or String for DPT10 time') + break + } + case 'number': + value = new Date(value) - return Buffer.from([(dow << 5) + hour, minute, second]); - }, + default: + dow = ((value.getDay() - 7) % 7) + 7 + hour = value.getHours() + minute = value.getMinutes() + second = value.getSeconds() + } - // return a JS Date from a DPT10 payload, with DOW/hour/month/seconds set to the buffer values. - // The week/month/year are inherited from the current timestamp. - fromBuffer: (buf) => { - if (buf.length != 3) - return log.warn("DPT10: Buffer should be 3 bytes long"); - const [dnh, minutes, seconds] = buf; - const dow = (dnh & 0b11100000) >> 5; - const hours = dnh & 0b00011111; - if ( - hours < 0 || - hours > 23 || - minutes < 0 || - minutes > 59 || - seconds < 0 || - seconds > 59 - ) - return log.warn( - "DPT10: buffer %j (decoded as %d:%d:%d) is not a valid time", - buf, - hours, - minutes, - seconds - ); + return Buffer.from([(dow << 5) + hour, minute, second]) + }, - const d = new Date(); - if (d.getDay() !== dow) - // adjust day of month to get the day of week right - d.setDate(d.getDate() + dow - d.getDay()); - // TODO: Shouldn't this be UTCHours? - d.setHours(hours, minutes, seconds); - return d; - }, + // return a JS Date from a DPT10 payload, with DOW/hour/month/seconds set to the buffer values. + // The week/month/year are inherited from the current timestamp. + fromBuffer: (buf) => { + if (buf.length !== 3) + return log.warn('DPT10: Buffer should be 3 bytes long') + const [dnh, minutes, seconds] = buf + const dow = (dnh & 0b11100000) >> 5 + const hours = dnh & 0b00011111 + if ( + hours < 0 || + hours > 23 || + minutes < 0 || + minutes > 59 || + seconds < 0 || + seconds > 59 + ) + return log.warn( + 'DPT10: buffer %j (decoded as %d:%d:%d) is not a valid time', + buf, + hours, + minutes, + seconds, + ) - // DPT10 base type info - basetype: { - bitlength: 24, - valuetype: "composite", - desc: "day of week + time of day", - }, + const d = new Date() + if (d.getDay() !== dow) + // adjust day of month to get the day of week right + d.setDate(d.getDate() + dow - d.getDay()) + // TODO: Shouldn't this be UTCHours? + d.setHours(hours, minutes, seconds) + return d + }, - // DPT10 subtypes info - subtypes: { - // 10.001 time of day - "001": { - name: "DPT_TimeOfDay", - desc: "time of day", - }, - }, -}; + // DPT10 base type info + basetype: { + bitlength: 24, + valuetype: 'composite', + desc: 'day of week + time of day', + }, -export default config; + // DPT10 subtypes info + subtypes: { + // 10.001 time of day + '001': { + name: 'DPT_TimeOfDay', + desc: 'time of day', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt11.ts b/src/dptlib/dpt11.ts index 20bb538..d80b68e 100644 --- a/src/dptlib/dpt11.ts +++ b/src/dptlib/dpt11.ts @@ -3,83 +3,84 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT11.*: date // const config: DatapointConfig = { - id: "DPT11", - formatAPDU: (value) => { - if (value == null) return log.error("cannot write null value for DPT11"); - switch (typeof value) { - case "string": - case "number": - value = new Date(value); - break; - case "object": - // this expects the month property to be zero-based (January = 0, etc.) - if (value instanceof Date) break; - const { year, month, day } = value; - value = new Date(parseInt(year), parseInt(month), parseInt(day)); - } - if (isNaN(value.getDate())) - return log.error( - "Must supply a numeric timestamp, Date or String object for DPT11 Date" - ); + id: 'DPT11', + formatAPDU: (value) => { + if (value == null) return log.error('cannot write null value for DPT11') + switch (typeof value) { + case 'string': + case 'number': + value = new Date(value) + break + case 'object': { + // this expects the month property to be zero-based (January = 0, etc.) + if (value instanceof Date) break + const { year, month, day } = value + value = new Date(parseInt(year), parseInt(month), parseInt(day)) + } + } + if (isNaN(value.getDate())) + return log.error( + 'Must supply a numeric timestamp, Date or String object for DPT11 Date', + ) - const year = value.getFullYear(); - return Buffer.from([ - value.getDate(), - value.getMonth() + 1, - year - (year >= 2000 ? 2000 : 1900), - ]); - }, + const year = value.getFullYear() + return Buffer.from([ + value.getDate(), + value.getMonth() + 1, + year - (year >= 2000 ? 2000 : 1900), + ]) + }, - fromBuffer: (buf) => { - if (buf.length != 3) return log.error("Buffer should be 3 bytes long"); - const day = buf[0] & 31; //0b00011111); - const month = buf[1] & 15; //0b00001111); - let year = buf[2] & 127; //0b01111111); - year = year + (year > 89 ? 1900 : 2000); - if ( - day < 1 || - day > 31 || - month < 1 || - month > 12 || - year < 1990 || - year > 2089 - ) { - log.error( - "%j => %d/%d/%d is not valid date according to DPT11, setting to 1990/01/01", - buf, - day, - month, - year - ); - //return new Date(1990, 01, 01); - throw new Error("Error converting date buffer to Date object."); - } - return new Date(year, month - 1, day); - }, + fromBuffer: (buf) => { + if (buf.length !== 3) return log.error('Buffer should be 3 bytes long') + const day = buf[0] & 31 // 0b00011111); + const month = buf[1] & 15 // 0b00001111); + let year = buf[2] & 127 // 0b01111111); + year += year > 89 ? 1900 : 2000 + if ( + day < 1 || + day > 31 || + month < 1 || + month > 12 || + year < 1990 || + year > 2089 + ) { + log.error( + '%j => %d/%d/%d is not valid date according to DPT11, setting to 1990/01/01', + buf, + day, + month, + year, + ) + // return new Date(1990, 01, 01); + throw new Error('Error converting date buffer to Date object.') + } + return new Date(year, month - 1, day) + }, - // DPT11 base type info - basetype: { - bitlength: 24, - valuetype: "composite", - desc: "3-byte date value", - }, + // DPT11 base type info + basetype: { + bitlength: 24, + valuetype: 'composite', + desc: '3-byte date value', + }, - // DPT11 subtypes info - subtypes: { - // 11.001 date - "001": { - name: "DPT_Date", - desc: "Date", - }, - }, -}; + // DPT11 subtypes info + subtypes: { + // 11.001 date + '001': { + name: 'DPT_Date', + desc: 'Date', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt12.ts b/src/dptlib/dpt12.ts index 011f92b..79f7612 100644 --- a/src/dptlib/dpt12.ts +++ b/src/dptlib/dpt12.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // // DPT12.*: 4-byte unsigned value @@ -11,21 +11,21 @@ import { DatapointConfig } from "."; // DPT12 base type info const config: DatapointConfig = { - id: "DPT12", - basetype: { - bitlength: 32, - signedness: "unsigned", - valuetype: "basic", - desc: "4-byte unsigned value", - }, - // DPT12 subtype info - subtypes: { - // 12.001 counter pulses - "001": { - name: "DPT_Value_4_Ucount", - desc: "counter pulses", - }, - }, -}; + id: 'DPT12', + basetype: { + bitlength: 32, + signedness: 'unsigned', + valuetype: 'basic', + desc: '4-byte unsigned value', + }, + // DPT12 subtype info + subtypes: { + // 12.001 counter pulses + '001': { + name: 'DPT_Value_4_Ucount', + desc: 'counter pulses', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt13.ts b/src/dptlib/dpt13.ts index c2b55c7..9466fb3 100644 --- a/src/dptlib/dpt13.ts +++ b/src/dptlib/dpt13.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // // DPT13: 4-byte signed value @@ -11,79 +11,79 @@ import { DatapointConfig } from "."; // DPT13 base type info const config: DatapointConfig = { - id: "DPT13", - basetype: { - bitlength: 32, - signedness: "signed", - valuetype: "basic", - desc: "4-byte signed value", - range: [-Math.pow(2, 31), Math.pow(2, 31) - 1], - }, + id: 'DPT13', + basetype: { + bitlength: 32, + signedness: 'signed', + valuetype: 'basic', + desc: '4-byte signed value', + range: [-(2 ** 31), 2 ** 31 - 1], + }, - // DPT13 subtypes - subtypes: { - // 13.001 counter pulses (signed) - "001": { - name: "DPT_Value_4_Count", - desc: "counter pulses (signed)", - unit: "pulses", - }, + // DPT13 subtypes + subtypes: { + // 13.001 counter pulses (signed) + '001': { + name: 'DPT_Value_4_Count', + desc: 'counter pulses (signed)', + unit: 'pulses', + }, - "002": { - name: "DPT_Value_Activation_Energy", - desc: "activation energy (J/mol)", - unit: "J/mol", - }, + '002': { + name: 'DPT_Value_Activation_Energy', + desc: 'activation energy (J/mol)', + unit: 'J/mol', + }, - // 13.010 active energy (Wh) - "010": { - name: "DPT_ActiveEnergy", - desc: "active energy (Wh)", - unit: "Wh", - }, + // 13.010 active energy (Wh) + '010': { + name: 'DPT_ActiveEnergy', + desc: 'active energy (Wh)', + unit: 'Wh', + }, - // 13.011 apparent energy (VAh) - "011": { - name: "DPT_ApparantEnergy", - desc: "apparent energy (VAh)", - unit: "VAh", - }, + // 13.011 apparent energy (VAh) + '011': { + name: 'DPT_ApparantEnergy', + desc: 'apparent energy (VAh)', + unit: 'VAh', + }, - // 13.012 reactive energy (VARh) - "012": { - name: "DPT_ReactiveEnergy", - desc: "reactive energy (VARh)", - unit: "VARh", - }, + // 13.012 reactive energy (VARh) + '012': { + name: 'DPT_ReactiveEnergy', + desc: 'reactive energy (VARh)', + unit: 'VARh', + }, - // 13.013 active energy (KWh) - "013": { - name: "DPT_ActiveEnergy_kWh", - desc: "active energy (kWh)", - unit: "kWh", - }, + // 13.013 active energy (KWh) + '013': { + name: 'DPT_ActiveEnergy_kWh', + desc: 'active energy (kWh)', + unit: 'kWh', + }, - // 13.014 apparent energy (kVAh) - "014": { - name: "DPT_ApparantEnergy_kVAh", - desc: "apparent energy (kVAh)", - unit: "VAh", - }, + // 13.014 apparent energy (kVAh) + '014': { + name: 'DPT_ApparantEnergy_kVAh', + desc: 'apparent energy (kVAh)', + unit: 'VAh', + }, - // 13.015 reactive energy (kVARh) - "015": { - name: "DPT_ReactiveEnergy_kVARh", - desc: "reactive energy (kVARh)", - unit: "kVARh", - }, + // 13.015 reactive energy (kVARh) + '015': { + name: 'DPT_ReactiveEnergy_kVARh', + desc: 'reactive energy (kVARh)', + unit: 'kVARh', + }, - // 13.100 time lag(s) - "100": { - name: "DPT_LongDeltaTimeSec", - desc: "time lag(s)", - unit: "s", - }, - }, -}; + // 13.100 time lag(s) + '100': { + name: 'DPT_LongDeltaTimeSec', + desc: 'time lag(s)', + unit: 's', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt14.ts b/src/dptlib/dpt14.ts index 4ff8b16..db83d9d 100644 --- a/src/dptlib/dpt14.ts +++ b/src/dptlib/dpt14.ts @@ -2,10 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT14.*: 4-byte floating point value @@ -15,157 +15,157 @@ const log = logger.get(); * the case for 32-bit floating point is simple... */ const config: DatapointConfig = { - id: "DPT14", - formatAPDU: (value) => { - if (value == null || typeof value != "number") - log.error("DPT14: Must supply a number value"); - const apdu_data = Buffer.alloc(4); - apdu_data.writeFloatBE(value, 0); - return apdu_data; - }, - - fromBuffer: (buf) => { - if (buf.length != 4) log.warn("DPT14: Buffer should be 4 bytes long"); - return buf.readFloatBE(0); - }, - - // DPT14 base type info - basetype: { - bitlength: 32, - valuetype: "basic", - range: [0, Math.pow(2, 32)], - desc: "32-bit floating point value", - }, - - // DPT14 subtypes info - subtypes: { - // TODO - "007": { - name: "DPT_Value_AngleDeg°", - desc: "angle, degree", - unit: "°", - }, - - "019": { - name: "DPT_Value_Electric_Current", - desc: "electric current", - unit: "A", - }, - - "027": { - name: "DPT_Value_Electric_Potential", - desc: "electric potential", - unit: "V", - }, - - "028": { - name: "DPT_Value_Electric_PotentialDifference", - desc: "electric potential difference", - unit: "V", - }, - - "031": { - name: "DPT_Value_Energ", - desc: "energy", - unit: "J", - }, - - "032": { - name: "DPT_Value_Force", - desc: "force", - unit: "N", - }, - - "033": { - name: "DPT_Value_Frequency", - desc: "frequency", - unit: "Hz", - }, - - "036": { - name: "DPT_Value_Heat_FlowRate", - desc: "heat flow rate", - unit: "W", - }, - - "037": { - name: "DPT_Value_Heat_Quantity", - desc: "heat, quantity of", - unit: "J", - }, - - "038": { - name: "DPT_Value_Impedance", - desc: "impedance", - unit: "Ω", - }, - - "039": { - name: "DPT_Value_Length", - desc: "length", - unit: "m", - }, - - "051": { - name: "DPT_Value_Mass", - desc: "mass", - unit: "kg", - }, - - "056": { - name: "DPT_Value_Power", - desc: "power", - unit: "W", - }, - - "065": { - name: "DPT_Value_Speed", - desc: "speed", - unit: "m/s", - }, - - "066": { - name: "DPT_Value_Stress", - desc: "stress", - unit: "Pa", - }, - - "067": { - name: "DPT_Value_Surface_Tension", - desc: "surface tension", - unit: "1/Nm", - }, - - "068": { - name: "DPT_Value_Common_Temperature", - desc: "temperature, common", - unit: "°C", - }, - - "069": { - name: "DPT_Value_Absolute_Temperature", - desc: "temperature (absolute)", - unit: "K", - }, - - "070": { - name: "DPT_Value_TemperatureDifference", - desc: "temperature difference", - unit: "K", - }, - - "078": { - name: "DPT_Value_Weight", - desc: "weight", - unit: "N", - }, - - "079": { - name: "DPT_Value_Work", - desc: "work", - unit: "J", - }, - }, -}; - -export default config; + id: 'DPT14', + formatAPDU: (value) => { + if (value == null || typeof value !== 'number') + log.error('DPT14: Must supply a number value') + const apdu_data = Buffer.alloc(4) + apdu_data.writeFloatBE(value, 0) + return apdu_data + }, + + fromBuffer: (buf) => { + if (buf.length !== 4) log.warn('DPT14: Buffer should be 4 bytes long') + return buf.readFloatBE(0) + }, + + // DPT14 base type info + basetype: { + bitlength: 32, + valuetype: 'basic', + range: [0, 2 ** 32], + desc: '32-bit floating point value', + }, + + // DPT14 subtypes info + subtypes: { + // TODO + '007': { + name: 'DPT_Value_AngleDeg°', + desc: 'angle, degree', + unit: '°', + }, + + '019': { + name: 'DPT_Value_Electric_Current', + desc: 'electric current', + unit: 'A', + }, + + '027': { + name: 'DPT_Value_Electric_Potential', + desc: 'electric potential', + unit: 'V', + }, + + '028': { + name: 'DPT_Value_Electric_PotentialDifference', + desc: 'electric potential difference', + unit: 'V', + }, + + '031': { + name: 'DPT_Value_Energ', + desc: 'energy', + unit: 'J', + }, + + '032': { + name: 'DPT_Value_Force', + desc: 'force', + unit: 'N', + }, + + '033': { + name: 'DPT_Value_Frequency', + desc: 'frequency', + unit: 'Hz', + }, + + '036': { + name: 'DPT_Value_Heat_FlowRate', + desc: 'heat flow rate', + unit: 'W', + }, + + '037': { + name: 'DPT_Value_Heat_Quantity', + desc: 'heat, quantity of', + unit: 'J', + }, + + '038': { + name: 'DPT_Value_Impedance', + desc: 'impedance', + unit: 'Ω', + }, + + '039': { + name: 'DPT_Value_Length', + desc: 'length', + unit: 'm', + }, + + '051': { + name: 'DPT_Value_Mass', + desc: 'mass', + unit: 'kg', + }, + + '056': { + name: 'DPT_Value_Power', + desc: 'power', + unit: 'W', + }, + + '065': { + name: 'DPT_Value_Speed', + desc: 'speed', + unit: 'm/s', + }, + + '066': { + name: 'DPT_Value_Stress', + desc: 'stress', + unit: 'Pa', + }, + + '067': { + name: 'DPT_Value_Surface_Tension', + desc: 'surface tension', + unit: '1/Nm', + }, + + '068': { + name: 'DPT_Value_Common_Temperature', + desc: 'temperature, common', + unit: '°C', + }, + + '069': { + name: 'DPT_Value_Absolute_Temperature', + desc: 'temperature (absolute)', + unit: 'K', + }, + + '070': { + name: 'DPT_Value_TemperatureDifference', + desc: 'temperature difference', + unit: 'K', + }, + + '078': { + name: 'DPT_Value_Weight', + desc: 'weight', + unit: 'N', + }, + + '079': { + name: 'DPT_Value_Work', + desc: 'work', + unit: 'J', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt15.ts b/src/dptlib/dpt15.ts index ae557ac..91e2de8 100644 --- a/src/dptlib/dpt15.ts +++ b/src/dptlib/dpt15.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // // DPT15.*: Access data @@ -14,20 +14,20 @@ import { DatapointConfig } from "."; // DPT15 base type info const config: DatapointConfig = { - id: "DPT15", - basetype: { - bitlength: 32, - valuetype: "basic", - desc: "4-byte access control data", - }, + id: 'DPT15', + basetype: { + bitlength: 32, + valuetype: 'basic', + desc: '4-byte access control data', + }, - // DPT15 subtypes info - subtypes: { - "000": { - name: "DPT_Access_Data", - desc: "Access Data", - }, - }, -}; + // DPT15 subtypes info + subtypes: { + '000': { + name: 'DPT_Access_Data', + desc: 'Access Data', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt16.ts b/src/dptlib/dpt16.ts index f4b1c1d..4add9c0 100644 --- a/src/dptlib/dpt16.ts +++ b/src/dptlib/dpt16.ts @@ -3,52 +3,52 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT16: ASCII string (max 14 chars) // const config: DatapointConfig = { - id: "DPT16", - formatAPDU: (value) => { - if (typeof value !== "string") - return log.warn("Must supply a string value"); - - const buf = Buffer.alloc(14); - buf.write(value, "ascii"); - return buf; - }, - - fromBuffer: (buf) => buf.toString("ascii"), - // DPT16 basetype info - basetype: { - bitlength: 14 * 8, - valuetype: "basic", - desc: "14-character string", - }, - - // DPT9 subtypes - subtypes: { - // 16.000 ASCII string - "000": { - use: "G", - name: "DPT_String_ASCII", - desc: "ASCII string", - force_encoding: "US-ASCII", - }, - - // 16.001 ISO-8859-1 string - "001": { - use: "G", - name: "DPT_String_8859_1", - desc: "ISO-8859-1 string", - force_encoding: "ISO-8859-1", - }, - }, -}; - -export default config; + id: 'DPT16', + formatAPDU: (value) => { + if (typeof value !== 'string') + return log.warn('Must supply a string value') + + const buf = Buffer.alloc(14) + buf.write(value, 'ascii') + return buf + }, + + fromBuffer: (buf) => buf.toString('ascii'), + // DPT16 basetype info + basetype: { + bitlength: 14 * 8, + valuetype: 'basic', + desc: '14-character string', + }, + + // DPT9 subtypes + subtypes: { + // 16.000 ASCII string + '000': { + use: 'G', + name: 'DPT_String_ASCII', + desc: 'ASCII string', + force_encoding: 'US-ASCII', + }, + + // 16.001 ISO-8859-1 string + '001': { + use: 'G', + name: 'DPT_String_8859_1', + desc: 'ISO-8859-1 string', + force_encoding: 'ISO-8859-1', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt17.ts b/src/dptlib/dpt17.ts index 329cc14..5cfd0b7 100644 --- a/src/dptlib/dpt17.ts +++ b/src/dptlib/dpt17.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // // DPT17: Scene number @@ -12,18 +12,18 @@ import { DatapointConfig } from "."; // TODO: implement fromBuffer, formatAPDU const config: DatapointConfig = { - id: "DPT17", - // DPT17 basetype info - basetype: { - bitlength: 8, - valuetype: "basic", - desc: "scene number", - }, - // DPT17 subtypes - subtypes: { - // 17.001 Scene number - "001": { use: "G", name: "DPT_SceneNumber", desc: "Scene Number" }, - }, -}; + id: 'DPT17', + // DPT17 basetype info + basetype: { + bitlength: 8, + valuetype: 'basic', + desc: 'scene number', + }, + // DPT17 subtypes + subtypes: { + // 17.001 Scene number + '001': { use: 'G', name: 'DPT_SceneNumber', desc: 'Scene Number' }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts index d5b455d..f54da54 100644 --- a/src/dptlib/dpt18.ts +++ b/src/dptlib/dpt18.ts @@ -3,8 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' +import { hasProp } from 'src/utils' + // // DPT18: 8-bit Scene Control // @@ -25,66 +27,70 @@ import { DatapointConfig } from "."; // TODO: implement fromBuffer, formatAPDU -const log = logger.get(); +const log = logger.get() const config: DatapointConfig = { - id: "DPT18", - formatAPDU: function (value) { - if (value == null) log.warn("DPT18: cannot write null value"); - else { - var apdu_data = Buffer.alloc(1); - if ( - typeof value == "object" && - value.hasOwnProperty("save_recall") && - value.hasOwnProperty("scenenumber") - ) { - var sSceneNumberbinary = ((value.scenenumber - 1) >>> 0).toString(2); - var sVal = - value.save_recall + "0" + sSceneNumberbinary.padStart(6, "0"); - //console.log("BANANA SEND HEX " + sVal.toString("hex").toUpperCase()) - apdu_data[0] = parseInt(sVal, 2); // 0b10111111; - } else { - log.error( - "DPT18: Must supply a value object of {save_recall, scenenumber}" - ); - } - return apdu_data; - } - }, + id: 'DPT18', + formatAPDU(value) { + if (value == null) log.warn('DPT18: cannot write null value') + else { + const apdu_data = Buffer.alloc(1) + if ( + typeof value === 'object' && + hasProp(value, 'save_recall') && + hasProp(value, 'scenenumber') + ) { + const sSceneNumberbinary = ( + (value.scenenumber - 1) >>> + 0 + ).toString(2) + const sVal = `${ + value.save_recall + }0${sSceneNumberbinary.padStart(6, '0')}` + // console.log("BANANA SEND HEX " + sVal.toString("hex").toUpperCase()) + apdu_data[0] = parseInt(sVal, 2) // 0b10111111; + } else { + log.error( + 'DPT18: Must supply a value object of {save_recall, scenenumber}', + ) + } + return apdu_data + } + }, - fromBuffer: function (buf) { - //console.log("BANANA BUFF RECEIVE HEX " + buf.toString("hex").toUpperCase()) - if (buf.length != 1) { - log.error("DP18: Buffer should be 1 byte long"); - } else { - var sBit = parseInt(buf.toString("hex").toUpperCase(), 16) - .toString(2) - .padStart(8, "0"); // Get bit from hex - //console.log("BANANA BUFF RECEIVE BIT " + sBit) - return { - save_recall: sBit.substring(0, 1), - scenenumber: parseInt(sBit.substring(2), 2) + 1, - }; - } - }, - // DPT18 basetype info - basetype: { - bitlength: 8, - valuetype: "composite", - desc: "8-bit Scene Activate/Learn + number", - }, + fromBuffer(buf) { + // console.log("BANANA BUFF RECEIVE HEX " + buf.toString("hex").toUpperCase()) + if (buf.length !== 1) { + log.error('DP18: Buffer should be 1 byte long') + } else { + const sBit = parseInt(buf.toString('hex').toUpperCase(), 16) + .toString(2) + .padStart(8, '0') // Get bit from hex + // console.log("BANANA BUFF RECEIVE BIT " + sBit) + return { + save_recall: sBit.substring(0, 1), + scenenumber: parseInt(sBit.substring(2), 2) + 1, + } + } + }, + // DPT18 basetype info + basetype: { + bitlength: 8, + valuetype: 'composite', + desc: '8-bit Scene Activate/Learn + number', + }, - // DPT9 subtypes - subtypes: { - // 9.001 temperature (oC) - "001": { - name: "DPT_SceneControl", - desc: "scene control", - }, - }, -}; + // DPT9 subtypes + subtypes: { + // 9.001 temperature (oC) + '001': { + name: 'DPT_SceneControl', + desc: 'scene control', + }, + }, +} -export default config; +export default config /* 02/April/2020 Supergiovane diff --git a/src/dptlib/dpt19.ts b/src/dptlib/dpt19.ts index 5514cce..dc269cc 100644 --- a/src/dptlib/dpt19.ts +++ b/src/dptlib/dpt19.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // TODO: implement fromBuffer, formatAPDU // @@ -14,51 +14,51 @@ const log = logger.get(); // const config: DatapointConfig = { - id: "DPT19", - formatAPDU: (value) => { - if (!(value instanceof Date)) - return log.error("DPT19: Must supply a Date object"); + id: 'DPT19', + formatAPDU: (value) => { + if (!(value instanceof Date)) + return log.error('DPT19: Must supply a Date object') - // Sunday is 0 in Javascript, but 7 in KNX. - const day = value.getDay() === 0 ? 7 : value.getDay(); - return Buffer.from([ - value.getFullYear() - 1900, - value.getMonth() + 1, - value.getDate(), - (day << 5) + value.getHours(), - value.getMinutes(), - value.getSeconds(), - 0, - 0, - ]); - }, + // Sunday is 0 in Javascript, but 7 in KNX. + const day = value.getDay() === 0 ? 7 : value.getDay() + return Buffer.from([ + value.getFullYear() - 1900, + value.getMonth() + 1, + value.getDate(), + (day << 5) + value.getHours(), + value.getMinutes(), + value.getSeconds(), + 0, + 0, + ]) + }, - fromBuffer: (buf) => { - if (buf.length !== 8) - return log.warn("DPT19: Buffer should be 8 bytes long"); - return new Date( - buf[0] + 1900, - buf[1] - 1, - buf[2], - buf[3] & 0b00011111, - buf[4], - buf[5] - ); - }, + fromBuffer: (buf) => { + if (buf.length !== 8) + return log.warn('DPT19: Buffer should be 8 bytes long') + return new Date( + buf[0] + 1900, + buf[1] - 1, + buf[2], + buf[3] & 0b00011111, + buf[4], + buf[5], + ) + }, - basetype: { - bitlength: 64, - valuetype: "composite", - desc: "8-byte Date+Time", - }, + basetype: { + bitlength: 64, + valuetype: 'composite', + desc: '8-byte Date+Time', + }, - subtypes: { - // 19.001 - "001": { - name: "DPT_DateTime", - desc: "datetime", - }, - }, -}; + subtypes: { + // 19.001 + '001': { + name: 'DPT_DateTime', + desc: 'datetime', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt2.ts b/src/dptlib/dpt2.ts index 307e690..ea9eabd 100644 --- a/src/dptlib/dpt2.ts +++ b/src/dptlib/dpt2.ts @@ -3,150 +3,153 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; -import KnxLog from "../KnxLog"; +import { hasProp } from 'src/utils' +import type { DatapointConfig } from '.' -const log = KnxLog.get(); +import KnxLog from '../KnxLog' + +const log = KnxLog.get() interface Dpt2Value { - priority: boolean | number; - data: boolean | number; + priority: boolean | number + data: boolean | number } const config: DatapointConfig = { - id: "DPT2", - - // DPT2 frame description. - // Always 8-bit aligned. - formatAPDU: (value: Dpt2Value) => { - if (value == null) return log.error("DPT2: cannot write null value"); - - if ( - typeof value === "object" && - value.hasOwnProperty("priority") && - value.hasOwnProperty("data") - ) - return Buffer.from([ - ((value.priority as number) << 1) + (value.data as number & 0b00000001), - ]); - - log.error("DPT2: Must supply an value {priority:, data:}"); - // FIXME: should this return zero buffer when error? Or nothing? - return Buffer.from([0]); - }, - - fromBuffer: (buf) => { - if (buf.length !== 1) return log.error("Buffer should be 1 byte long"); - - return { - priority: (buf[0] & 0b00000010) >> 1, - data: buf[0] & 0b00000001, - }; - }, - - // DPT basetype info hash - basetype: { - bitlength: 2, - valuetype: "composite", - desc: "1-bit value with priority", - }, - - // DPT subtypes info hash - subtypes: { - // 2.001 switch control - "001": { - use: "G", - name: "DPT_Switch_Control", - desc: "switch with priority", - enc: { 0: "Off", 1: "On" }, - }, - // 2.002 boolean control - "002": { - use: "G", - name: "DPT_Bool_Control", - desc: "boolean with priority", - enc: { 0: "false", 1: "true" }, - }, - // 2.003 enable control - "003": { - use: "FB", - name: "DPT_Emable_Control", - desc: "enable with priority", - enc: { 0: "Disabled", 1: "Enabled" }, - }, - - // 2.004 ramp control - "004": { - use: "FB", - name: "DPT_Ramp_Control", - desc: "ramp with priority", - enc: { 0: "No ramp", 1: "Ramp" }, - }, - - // 2.005 alarm control - "005": { - use: "FB", - name: "DPT_Alarm_Control", - desc: "alarm with priority", - enc: { 0: "No alarm", 1: "Alarm" }, - }, - - // 2.006 binary value control - "006": { - use: "FB", - name: "DPT_BinaryValue_Control", - desc: "binary value with priority", - enc: { 0: "Off", 1: "On" }, - }, - - // 2.007 step control - "007": { - use: "FB", - name: "DPT_Step_Control", - desc: "step with priority", - enc: { 0: "Off", 1: "On" }, - }, - - // 2.008 Direction1 control - "008": { - use: "FB", - name: "DPT_Direction1_Control", - desc: "direction 1 with priority", - enc: { 0: "Off", 1: "On" }, - }, - - // 2.009 Direction2 control - "009": { - use: "FB", - name: "DPT_Direction2_Control", - desc: "direction 2 with priority", - enc: { 0: "Off", 1: "On" }, - }, - - // 2.010 start control - "010": { - use: "FB", - name: "DPT_Start_Control", - desc: "start with priority", - enc: { 0: "No control", 1: "No control", 2: "Off", 3: "On" }, - }, - - // 2.011 state control - "011": { - use: "FB", - name: "DPT_Switch_Control", - desc: "switch", - enc: { 0: "No control", 1: "No control", 2: "Off", 3: "On" }, - }, - - // 2.012 invert control - "012": { - use: "FB", - name: "DPT_Switch_Control", - desc: "switch", - enc: { 0: "No control", 1: "No control", 2: "Off", 3: "On" }, - }, - }, -}; - -export default config; + id: 'DPT2', + + // DPT2 frame description. + // Always 8-bit aligned. + formatAPDU: (value: Dpt2Value) => { + if (value == null) return log.error('DPT2: cannot write null value') + + if ( + typeof value === 'object' && + hasProp(value, 'priority') && + hasProp(value, 'data') + ) + return Buffer.from([ + ((value.priority as number) << 1) + + (value.data as number & 0b00000001), + ]) + + log.error('DPT2: Must supply an value {priority:, data:}') + // FIXME: should this return zero buffer when error? Or nothing? + return Buffer.from([0]) + }, + + fromBuffer: (buf) => { + if (buf.length !== 1) return log.error('Buffer should be 1 byte long') + + return { + priority: (buf[0] & 0b00000010) >> 1, + data: buf[0] & 0b00000001, + } + }, + + // DPT basetype info hash + basetype: { + bitlength: 2, + valuetype: 'composite', + desc: '1-bit value with priority', + }, + + // DPT subtypes info hash + subtypes: { + // 2.001 switch control + '001': { + use: 'G', + name: 'DPT_Switch_Control', + desc: 'switch with priority', + enc: { 0: 'Off', 1: 'On' }, + }, + // 2.002 boolean control + '002': { + use: 'G', + name: 'DPT_Bool_Control', + desc: 'boolean with priority', + enc: { 0: 'false', 1: 'true' }, + }, + // 2.003 enable control + '003': { + use: 'FB', + name: 'DPT_Emable_Control', + desc: 'enable with priority', + enc: { 0: 'Disabled', 1: 'Enabled' }, + }, + + // 2.004 ramp control + '004': { + use: 'FB', + name: 'DPT_Ramp_Control', + desc: 'ramp with priority', + enc: { 0: 'No ramp', 1: 'Ramp' }, + }, + + // 2.005 alarm control + '005': { + use: 'FB', + name: 'DPT_Alarm_Control', + desc: 'alarm with priority', + enc: { 0: 'No alarm', 1: 'Alarm' }, + }, + + // 2.006 binary value control + '006': { + use: 'FB', + name: 'DPT_BinaryValue_Control', + desc: 'binary value with priority', + enc: { 0: 'Off', 1: 'On' }, + }, + + // 2.007 step control + '007': { + use: 'FB', + name: 'DPT_Step_Control', + desc: 'step with priority', + enc: { 0: 'Off', 1: 'On' }, + }, + + // 2.008 Direction1 control + '008': { + use: 'FB', + name: 'DPT_Direction1_Control', + desc: 'direction 1 with priority', + enc: { 0: 'Off', 1: 'On' }, + }, + + // 2.009 Direction2 control + '009': { + use: 'FB', + name: 'DPT_Direction2_Control', + desc: 'direction 2 with priority', + enc: { 0: 'Off', 1: 'On' }, + }, + + // 2.010 start control + '010': { + use: 'FB', + name: 'DPT_Start_Control', + desc: 'start with priority', + enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, + }, + + // 2.011 state control + '011': { + use: 'FB', + name: 'DPT_Switch_Control', + desc: 'switch', + enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, + }, + + // 2.012 invert control + '012': { + use: 'FB', + name: 'DPT_Switch_Control', + desc: 'switch', + enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, + }, + }, +} + +export default config diff --git a/src/dptlib/dpt20.ts b/src/dptlib/dpt20.ts index 94a720d..ed0463e 100644 --- a/src/dptlib/dpt20.ts +++ b/src/dptlib/dpt20.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT20: 1-byte HVAC @@ -14,36 +14,36 @@ const log = logger.get(); // FIXME: help needed const config: DatapointConfig = { - id: "DPT20", - formatAPDU: (value) => { - log.debug("./knx/src/dpt20.js : input value = " + value); - return Buffer.from([value]); - }, - - fromBuffer: (buf) => { - if (buf.length !== 1) throw "Buffer should be 1 bytes long"; - const ret = buf.readUInt8(0); - log.debug(" dpt20.js fromBuffer : " + ret); - return ret; - }, - - basetype: { - bitlength: 8, - range: [undefined, undefined], // TODO: verify - valuetype: "basic", - desc: "1-byte", - }, - - subtypes: { - // 20.102 HVAC mode - 102: { - name: "HVAC_Mode", - desc: "", - unit: "", - scalar_range: [undefined, undefined], // TODO: verify - range: [undefined, undefined], // TODO: verify - }, - }, -}; - -export default config; + id: 'DPT20', + formatAPDU: (value) => { + log.debug(`./knx/src/dpt20.js : input value = ${value}`) + return Buffer.from([value]) + }, + + fromBuffer: (buf) => { + if (buf.length !== 1) throw Error('Buffer should be 1 bytes long') + const ret = buf.readUInt8(0) + log.debug(` dpt20.js fromBuffer : ${ret}`) + return ret + }, + + basetype: { + bitlength: 8, + range: [undefined, undefined], // TODO: verify + valuetype: 'basic', + desc: '1-byte', + }, + + subtypes: { + // 20.102 HVAC mode + 102: { + name: 'HVAC_Mode', + desc: '', + unit: '', + scalar_range: [undefined, undefined], // TODO: verify + range: [undefined, undefined], // TODO: verify + }, + }, +} + +export default config diff --git a/src/dptlib/dpt21.ts b/src/dptlib/dpt21.ts index 62a8e3f..2ce0c3a 100644 --- a/src/dptlib/dpt21.ts +++ b/src/dptlib/dpt21.ts @@ -2,10 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT21: 1-byte status @@ -19,69 +19,69 @@ const log = logger.get(); // FIXME: help needed const config: DatapointConfig = { - id: "DPT21", - formatAPDU: function (value) { - if (value == null) return log.error("DPT21: cannot write null value"); - log.debug("./knx/src/dpt21.js : input value = " + value); + id: 'DPT21', + formatAPDU(value) { + if (value == null) return log.error('DPT21: cannot write null value') + log.debug(`./knx/src/dpt21.js : input value = ${value}`) - //var apdu_data = Buffer.alloc(1); - //apdu_data[0] = value; - if (typeof value === "object") - return Buffer.from([ - value.outofservice + - (value.fault << 1) + - (value.overridden << 2) + - (value.inalarm << 3) + - (value.alarmeunack << 4), - ]); + // var apdu_data = Buffer.alloc(1); + // apdu_data[0] = value; + if (typeof value === 'object') + return Buffer.from([ + value.outofservice + + (value.fault << 1) + + (value.overridden << 2) + + (value.inalarm << 3) + + (value.alarmeunack << 4), + ]) - log.error("DPT21: Must supply a value which is an object"); - //return apdu_data; - return Buffer.from([0]); - }, + log.error('DPT21: Must supply a value which is an object') + // return apdu_data; + return Buffer.from([0]) + }, - fromBuffer: function (buf) { - if (buf.length != 1) return log.error("Buffer should be 1 bytes long"); - //if (buf.length != 1) throw "Buffer should be 1 bytes long"; - log.debug(" dpt21.js fromBuffer : " + buf); + fromBuffer(buf) { + if (buf.length !== 1) return log.error('Buffer should be 1 bytes long') + // if (buf.length != 1) throw "Buffer should be 1 bytes long"; + log.debug(` dpt21.js fromBuffer : ${buf}`) - //var ret = buf.readUInt8(0); + // var ret = buf.readUInt8(0); - return { - outofservice: buf[0] & 0b00000001, - fault: (buf[0] & 0b00000010) >> 1, - overridden: (buf[0] & 0b00000100) >> 2, - inalarm: (buf[0] & 0b00001000) >> 3, - alarmunack: (buf[0] & 0b00010000) >> 4, - }; - //return ret; - }, + return { + outofservice: buf[0] & 0b00000001, + fault: (buf[0] & 0b00000010) >> 1, + overridden: (buf[0] & 0b00000100) >> 2, + inalarm: (buf[0] & 0b00001000) >> 3, + alarmunack: (buf[0] & 0b00010000) >> 4, + } + // return ret; + }, - basetype: { - bitlength: 8, - range: [undefined, undefined], - valuetype: "composite", - desc: "1-byte", - }, + basetype: { + bitlength: 8, + range: [undefined, undefined], + valuetype: 'composite', + desc: '1-byte', + }, - subtypes: { - // 21.001 status - 5 bits - "001": { - name: "DPT_StatusGen", - desc: "General Status", - unit: "", - scalar_range: [undefined, undefined], - range: [undefined, undefined], - }, - // 21.002 control - 3 bits - "002": { - name: "DPT_Device_Control", - desc: "Device Control", - unit: "", - scalar_range: [undefined, undefined], - range: [undefined, undefined], - }, - }, -}; + subtypes: { + // 21.001 status - 5 bits + '001': { + name: 'DPT_StatusGen', + desc: 'General Status', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + // 21.002 control - 3 bits + '002': { + name: 'DPT_Device_Control', + desc: 'Device Control', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt232.ts b/src/dptlib/dpt232.ts index 8cae4a2..2eb02fd 100644 --- a/src/dptlib/dpt232.ts +++ b/src/dptlib/dpt232.ts @@ -3,56 +3,56 @@ * (C) 2016-2019 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT232: 3-byte RGB color array // MSB: Red, Green, LSB: Blue // const config: DatapointConfig = { - id: "DPT232", - formatAPDU: (value) => { - if (value == null) return log.error("DPT232: cannot write null value"); + id: 'DPT232', + formatAPDU: (value) => { + if (value == null) return log.error('DPT232: cannot write null value') - if (typeof value === "object") { - const { red, green, blue } = value; - if ( - red >= 0 && - red <= 255 && - green >= 0 && - green <= 255 && - blue >= 0 && - blue <= 255 - ) - return Buffer.from([red, green, blue]); - } - log.error( - "DPT232: Must supply an value {red:0..255, green:0.255, blue:0.255}" - ); - }, + if (typeof value === 'object') { + const { red, green, blue } = value + if ( + red >= 0 && + red <= 255 && + green >= 0 && + green <= 255 && + blue >= 0 && + blue <= 255 + ) + return Buffer.from([red, green, blue]) + } + log.error( + 'DPT232: Must supply an value {red:0..255, green:0.255, blue:0.255}', + ) + }, - fromBuffer: (buf) => { - const [red, green, blue] = buf; - return { red, green, blue }; - }, - basetype: { - bitlength: 3 * 8, - valuetype: "basic", - desc: "RGB array", - }, + fromBuffer: (buf) => { + const [red, green, blue] = buf + return { red, green, blue } + }, + basetype: { + bitlength: 3 * 8, + valuetype: 'basic', + desc: 'RGB array', + }, - subtypes: { - 600: { - name: "RGB", - desc: "RGB color triplet", - unit: "", - scalar_range: [undefined, undefined], - range: [undefined, undefined], - }, - }, -}; + subtypes: { + 600: { + name: 'RGB', + desc: 'RGB color triplet', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt237.ts b/src/dptlib/dpt237.ts index 0dba4de..43b7bee 100644 --- a/src/dptlib/dpt237.ts +++ b/src/dptlib/dpt237.ts @@ -3,82 +3,84 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' +import { hasProp } from 'src/utils' -const log = logger.get(); +const log = logger.get() // // DPT237: 2-byte unsigned value // const config: DatapointConfig = { - id: "DPT237", - formatAPDU: function (value) { - if (value == null) return log.error("DPT237: cannot write null value"); + id: 'DPT237', + formatAPDU(value) { + if (value == null) return log.error('DPT237: cannot write null value') - log.trace("dpt278.js : input value = " + value); + log.trace(`dpt278.js : input value = ${value}`) - var apdu_data = Buffer.alloc(2); + const apdu_data = Buffer.alloc(2) - //console.log("Buffer lenght: ", apdu_data.length); - if ( - typeof value === "object" && - value.hasOwnProperty("addresstype") && - value.hasOwnProperty("address") && - value.hasOwnProperty("readresponse") && - value.hasOwnProperty("lampfailure") && - value.hasOwnProperty("ballastfailure") && - value.hasOwnProperty("convertorerror") - ) { - // these are reversed as [0] is high and [1] low - apdu_data[1] = - (value.address & 0b00011111) + - (value.addresstype << 5) + - (value.readresponse << 6) + - (value.lampfailure << 7); - apdu_data[0] = - (value.ballastfailure & 0b00000001) + (value.convertorerror << 1); + // console.log("Buffer lenght: ", apdu_data.length); + if ( + typeof value === 'object' && + hasProp(value, 'addresstype') && + hasProp(value, 'address') && + hasProp(value, 'readresponse') && + hasProp(value, 'lampfailure') && + hasProp(value, 'ballastfailure') && + hasProp(value, 'convertorerror') + ) { + // these are reversed as [0] is high and [1] low + apdu_data[1] = + (value.address & 0b00011111) + + (value.addresstype << 5) + + (value.readresponse << 6) + + (value.lampfailure << 7) + apdu_data[0] = + (value.ballastfailure & 0b00000001) + + (value.convertorerror << 1) - return apdu_data; - } + return apdu_data + } - log.error( - "DPT237: Must supply an value {address:[0,63] or [0,15], address type:{0,1}, ...}" - ); + log.error( + 'DPT237: Must supply an value {address:[0,63] or [0,15], address type:{0,1}, ...}', + ) - return apdu_data; - }, + return apdu_data + }, - fromBuffer: function (buf) { - if (buf.length !== 2) return log.error("Buffer should be 2 byte long"); + fromBuffer(buf) { + if (buf.length !== 2) return log.error('Buffer should be 2 byte long') - return { - address: buf[1] & 0b00011111, - addresstype: (buf[1] & 0b00100000) >> 5, - readresponse: (buf[1] & 0b01000000) >> 6, - lampfailure: (buf[1] & 0b10000000) >> 7, - ballastfailure: buf[0] & 0b00000001, - convertorerror: (buf[0] & 0b00000010) >> 1, - }; - }, + return { + address: buf[1] & 0b00011111, + addresstype: (buf[1] & 0b00100000) >> 5, + readresponse: (buf[1] & 0b01000000) >> 6, + lampfailure: (buf[1] & 0b10000000) >> 7, + ballastfailure: buf[0] & 0b00000001, + convertorerror: (buf[0] & 0b00000010) >> 1, + } + }, - basetype: { - bitlength: 16, - range: [undefined, undefined], - valuetype: "composite", - desc: "2-byte", - }, + basetype: { + bitlength: 16, + range: [undefined, undefined], + valuetype: 'composite', + desc: '2-byte', + }, - subtypes: { - // 237.600 HVAC mode - "600": { - name: "HVAC_Mode", - desc: "", - unit: "", - scalar_range: [undefined, undefined], - range: [undefined, undefined], - }, - }, -}; + subtypes: { + // 237.600 HVAC mode + '600': { + name: 'HVAC_Mode', + desc: '', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt238.ts b/src/dptlib/dpt238.ts index ec32922..8c6b418 100644 --- a/src/dptlib/dpt238.ts +++ b/src/dptlib/dpt238.ts @@ -3,80 +3,80 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT238: 1-byte unsigned value // // DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) const config: DatapointConfig = { - id: "DPT238", - formatAPDU: (value) => { - const apdu_data = Buffer.from([value]); - log.trace( - "dpt238.js : input value = " + value + " apdu_data = " + apdu_data - ); - return apdu_data; - }, + id: 'DPT238', + formatAPDU: (value) => { + const apdu_data = Buffer.from([value]) + log.trace( + `dpt238.js : input value = ${value} apdu_data = ${apdu_data}`, + ) + return apdu_data + }, - fromBuffer: (buf) => buf[0], + fromBuffer: (buf) => buf[0], - basetype: { - bitlength: 8, - range: [undefined,undefined], - valuetype: "basic", - desc: "1-byte", - }, + basetype: { + bitlength: 8, + range: [undefined, undefined], + valuetype: 'basic', + desc: '1-byte', + }, - subtypes: { - // 20.102 HVAC mode - 102: { - name: "HVAC_Mode", - desc: "", - unit: "", - scalar_range: [undefined,undefined], - range: [undefined,undefined], - }, + subtypes: { + // 20.102 HVAC mode + 102: { + name: 'HVAC_Mode', + desc: '', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, - // 5.003 angle (degrees 0=0, ff=360) - "003": { - name: "DPT_Angle", - desc: "angle degrees", - unit: "°", - scalar_range: [0, 360], - }, + // 5.003 angle (degrees 0=0, ff=360) + '003': { + name: 'DPT_Angle', + desc: 'angle degrees', + unit: '°', + scalar_range: [0, 360], + }, - // 5.004 percentage (0..255%) - "004": { - name: "DPT_Percent_U8", - desc: "percent", - unit: "%", - }, + // 5.004 percentage (0..255%) + '004': { + name: 'DPT_Percent_U8', + desc: 'percent', + unit: '%', + }, - // 5.005 ratio (0..255) - "005": { - name: "DPT_DecimalFactor", - desc: "ratio", - unit: "ratio", - }, + // 5.005 ratio (0..255) + '005': { + name: 'DPT_DecimalFactor', + desc: 'ratio', + unit: 'ratio', + }, - // 5.006 tariff (0..255) - "006": { - name: "DPT_Tariff", - desc: "tariff", - unit: "tariff", - }, + // 5.006 tariff (0..255) + '006': { + name: 'DPT_Tariff', + desc: 'tariff', + unit: 'tariff', + }, - // 5.010 counter pulses (0..255) - "010": { - name: "DPT_Value_1_Ucount", - desc: "counter pulses", - unit: "pulses", - }, - }, -}; + // 5.010 counter pulses (0..255) + '010': { + name: 'DPT_Value_1_Ucount', + desc: 'counter pulses', + unit: 'pulses', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt3.ts b/src/dptlib/dpt3.ts index d041c99..900eda6 100644 --- a/src/dptlib/dpt3.ts +++ b/src/dptlib/dpt3.ts @@ -3,61 +3,66 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; -import KnxLog from "../KnxLog"; +import { hasProp } from 'src/utils' +import type { DatapointConfig } from '.' -const log = KnxLog.get(); +import KnxLog from '../KnxLog' + +const log = KnxLog.get() interface Dpt3Value { - decr_incr: number; - data: number; + decr_incr: number + data: number } const config: DatapointConfig = { - id: "DPT3", - formatAPDU: (value: Dpt3Value) => { - if (value == null) return log.warn("DPT3: cannot write null value"); + id: 'DPT3', + formatAPDU: (value: Dpt3Value) => { + if (value == null) return log.warn('DPT3: cannot write null value') - if ( - typeof value == "object" && - value.hasOwnProperty("decr_incr") && - value.hasOwnProperty("data") - ) - return Buffer.from([(value.decr_incr << 3) + (value.data & 0b00000111)]); + if ( + typeof value === 'object' && + hasProp(value, 'decr_incr') && + hasProp(value, 'data') + ) + return Buffer.from([ + (value.decr_incr << 3) + (value.data & 0b00000111), + ]) - log.error("Must supply a value object of {decr_incr, data}"); - // FIXME: should this return zero buffer when error? Or nothing? - return Buffer.from([0]); - }, - fromBuffer: (buf: Buffer) => { - if (buf.length != 1) return log.error("DPT3: Buffer should be 1 byte long"); + log.error('Must supply a value object of {decr_incr, data}') + // FIXME: should this return zero buffer when error? Or nothing? + return Buffer.from([0]) + }, + fromBuffer: (buf: Buffer) => { + if (buf.length !== 1) + return log.error('DPT3: Buffer should be 1 byte long') - return { - decr_incr: (buf[0] & 0b00001000) >> 3, - data: buf[0] & 0b00000111, - }; - }, - basetype: { - bitlength: 4, - valuetype: "composite", - desc: "4-bit relative dimming control", - }, - subtypes: { - // 3.007 dimming control - "007": { - name: "DPT_Control_Dimming", - desc: "dimming control", - }, + return { + decr_incr: (buf[0] & 0b00001000) >> 3, + data: buf[0] & 0b00000111, + } + }, + basetype: { + bitlength: 4, + valuetype: 'composite', + desc: '4-bit relative dimming control', + }, + subtypes: { + // 3.007 dimming control + '007': { + name: 'DPT_Control_Dimming', + desc: 'dimming control', + }, - // 3.008 blind control - "008": { - name: "DPT_Control_Blinds", - desc: "blinds control", - }, - }, -}; + // 3.008 blind control + '008': { + name: 'DPT_Control_Blinds', + desc: 'blinds control', + }, + }, +} -export default config; +export default config /* 2.6.3.5 Behavior diff --git a/src/dptlib/dpt4.ts b/src/dptlib/dpt4.ts index b781e60..1f71307 100644 --- a/src/dptlib/dpt4.ts +++ b/src/dptlib/dpt4.ts @@ -3,53 +3,55 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; -import KnxLog from "../KnxLog"; +import type { DatapointConfig } from '.' -const log = KnxLog.get(); +import KnxLog from '../KnxLog' + +const log = KnxLog.get() // // DPT4: 8-bit character // const config: DatapointConfig = { - id: "DPT4", - formatAPDU: (value: string): Buffer | void => { - if (value == null) return log.warn("DPT4: cannot write null value"); - - if (typeof value !== "string") - return log.warn("DPT4: Must supply a character or string"); - - const apdu_data: number = value.charCodeAt(0); - if (apdu_data > 255) - return log.warn("DPT4: must supply an ASCII character"); - - return Buffer.from([apdu_data]); - }, - fromBuffer: (buf: Buffer): string | void => { - if (buf.length != 1) return log.warn("DPT4: Buffer should be 1 byte long"); - - return String.fromCharCode(buf[0]); - }, - basetype: { - bitlength: 8, - valuetype: "basic", - desc: "8-bit character", - }, - subtypes: { - // 4.001 character (ASCII) - "001": { - name: "DPT_Char_ASCII", - desc: "ASCII character (0-127)", - range: [0, 127], - use: "G", - }, - // 4.002 character (ISO-8859-1) - "002": { - name: "DPT_Char_8859_1", - desc: "ISO-8859-1 character (0..255)", - use: "G", - }, - }, -}; - -export default config; + id: 'DPT4', + formatAPDU: (value: string): Buffer | void => { + if (value == null) return log.warn('DPT4: cannot write null value') + + if (typeof value !== 'string') + return log.warn('DPT4: Must supply a character or string') + + const apdu_data: number = value.charCodeAt(0) + if (apdu_data > 255) + return log.warn('DPT4: must supply an ASCII character') + + return Buffer.from([apdu_data]) + }, + fromBuffer: (buf: Buffer): string | void => { + if (buf.length !== 1) + return log.warn('DPT4: Buffer should be 1 byte long') + + return String.fromCharCode(buf[0]) + }, + basetype: { + bitlength: 8, + valuetype: 'basic', + desc: '8-bit character', + }, + subtypes: { + // 4.001 character (ASCII) + '001': { + name: 'DPT_Char_ASCII', + desc: 'ASCII character (0-127)', + range: [0, 127], + use: 'G', + }, + // 4.002 character (ISO-8859-1) + '002': { + name: 'DPT_Char_8859_1', + desc: 'ISO-8859-1 character (0..255)', + use: 'G', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt5.ts b/src/dptlib/dpt5.ts index 2c7f65d..16e3946 100644 --- a/src/dptlib/dpt5.ts +++ b/src/dptlib/dpt5.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // // DPT5: 8-bit unsigned value @@ -11,60 +11,60 @@ import { DatapointConfig } from "."; // DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) const config: DatapointConfig = { - id: "DPT5", - basetype: { - bitlength: 8, - signedness: "unsigned", - range: [0, 255], - valuetype: "basic", - desc: "8-bit unsigned value", - }, + id: 'DPT5', + basetype: { + bitlength: 8, + signedness: 'unsigned', + range: [0, 255], + valuetype: 'basic', + desc: '8-bit unsigned value', + }, - subtypes: { - // 5.001 percentage (0=0..ff=100%) - "001": { - name: "DPT_Scaling", - desc: "percent", - unit: "%", - scalar_range: [0, 100], - }, + subtypes: { + // 5.001 percentage (0=0..ff=100%) + '001': { + name: 'DPT_Scaling', + desc: 'percent', + unit: '%', + scalar_range: [0, 100], + }, - // 5.003 angle (degrees 0=0, ff=360) - "003": { - name: "DPT_Angle", - desc: "angle degrees", - unit: "°", - scalar_range: [0, 360], - }, + // 5.003 angle (degrees 0=0, ff=360) + '003': { + name: 'DPT_Angle', + desc: 'angle degrees', + unit: '°', + scalar_range: [0, 360], + }, - // 5.004 percentage (0..255%) - "004": { - name: "DPT_Percent_U8", - desc: "percent", - unit: "%", - }, + // 5.004 percentage (0..255%) + '004': { + name: 'DPT_Percent_U8', + desc: 'percent', + unit: '%', + }, - // 5.005 ratio (0..255) - "005": { - name: "DPT_DecimalFactor", - desc: "ratio", - unit: "ratio", - }, + // 5.005 ratio (0..255) + '005': { + name: 'DPT_DecimalFactor', + desc: 'ratio', + unit: 'ratio', + }, - // 5.006 tariff (0..255) - "006": { - name: "DPT_Tariff", - desc: "tariff", - unit: "tariff", - }, + // 5.006 tariff (0..255) + '006': { + name: 'DPT_Tariff', + desc: 'tariff', + unit: 'tariff', + }, - // 5.010 counter pulses (0..255) - "010": { - name: "DPT_Value_1_Ucount", - desc: "counter pulses", - unit: "pulses", - }, - }, -}; + // 5.010 counter pulses (0..255) + '010': { + name: 'DPT_Value_1_Ucount', + desc: 'counter pulses', + unit: 'pulses', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt6.ts b/src/dptlib/dpt6.ts index 88f3398..febfb35 100644 --- a/src/dptlib/dpt6.ts +++ b/src/dptlib/dpt6.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // Bitstruct to parse a DPT6 frame (8-bit signed integer) // Always 8-bit aligned. @@ -11,33 +11,33 @@ import { DatapointConfig } from "."; // DPT Basetype info const config: DatapointConfig = { - id: "DPT6", - basetype: { - bitlength: 8, - signedness: "signed", - valuetype: "basic", - desc: "8-bit signed value", - range: [-128, 127], - }, - - // DPT subtypes info - subtypes: { - // 6.001 percentage (-128%..127%) - "001": { - name: "DPT_Switch", - desc: "percent", - unit: "%", - }, - - // 6.002 counter pulses (-128..127) - "010": { - name: "DPT_Bool", - desc: "counter pulses", - unit: "pulses", - }, - - // - }, -}; - -export default config; + id: 'DPT6', + basetype: { + bitlength: 8, + signedness: 'signed', + valuetype: 'basic', + desc: '8-bit signed value', + range: [-128, 127], + }, + + // DPT subtypes info + subtypes: { + // 6.001 percentage (-128%..127%) + '001': { + name: 'DPT_Switch', + desc: 'percent', + unit: '%', + }, + + // 6.002 counter pulses (-128..127) + '010': { + name: 'DPT_Bool', + desc: 'counter pulses', + unit: 'pulses', + }, + + // + }, +} + +export default config diff --git a/src/dptlib/dpt7.ts b/src/dptlib/dpt7.ts index e679c7f..7822469 100644 --- a/src/dptlib/dpt7.ts +++ b/src/dptlib/dpt7.ts @@ -3,118 +3,118 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // // DPT7: 16-bit unsigned integer value // const config: DatapointConfig = { - id: "DPT7", - basetype: { - bitlength: 16, - signedness: "unsigned", - valuetype: "basic", - desc: "16-bit unsigned value", - }, + id: 'DPT7', + basetype: { + bitlength: 16, + signedness: 'unsigned', + valuetype: 'basic', + desc: '16-bit unsigned value', + }, - // DPT subtypes info - subtypes: { - // 7.001 pulses - "001": { - use: "G", - name: "DPT_Value_2_Ucount", - desc: "pulses", - unit: "pulses", - }, + // DPT subtypes info + subtypes: { + // 7.001 pulses + '001': { + use: 'G', + name: 'DPT_Value_2_Ucount', + desc: 'pulses', + unit: 'pulses', + }, - // 7.002 time(ms) - "002": { - use: "G", - name: "DPT_TimePeriodMsec", - desc: "time (ms)", - unit: "milliseconds", - }, + // 7.002 time(ms) + '002': { + use: 'G', + name: 'DPT_TimePeriodMsec', + desc: 'time (ms)', + unit: 'milliseconds', + }, - // 7.003 time (10ms) - "003": { - use: "G", - name: "DPT_TimePeriod10Msec", - desc: "time (10ms)", - unit: "centiseconds", - }, + // 7.003 time (10ms) + '003': { + use: 'G', + name: 'DPT_TimePeriod10Msec', + desc: 'time (10ms)', + unit: 'centiseconds', + }, - // 7.004 time (100ms) - "004": { - use: "G", - name: "DPT_TimePeriod100Msec", - desc: "time (100ms)", - unit: "deciseconds", - }, + // 7.004 time (100ms) + '004': { + use: 'G', + name: 'DPT_TimePeriod100Msec', + desc: 'time (100ms)', + unit: 'deciseconds', + }, - // 7.005 time (sec) - "005": { - use: "G", - name: "DPT_TimePeriodSec", - desc: "time (s)", - unit: "seconds", - }, + // 7.005 time (sec) + '005': { + use: 'G', + name: 'DPT_TimePeriodSec', + desc: 'time (s)', + unit: 'seconds', + }, - // 7.006 time (min) - "006": { - use: "G", - name: "DPT_TimePeriodMin", - desc: "time (min)", - unit: "minutes", - }, + // 7.006 time (min) + '006': { + use: 'G', + name: 'DPT_TimePeriodMin', + desc: 'time (min)', + unit: 'minutes', + }, - // 7.007 time (hour) - "007": { - use: "G", - name: "DPT_TimePeriodHrs", - desc: "time (hrs)", - unit: "hours", - }, + // 7.007 time (hour) + '007': { + use: 'G', + name: 'DPT_TimePeriodHrs', + desc: 'time (hrs)', + unit: 'hours', + }, - // 7.010 DPT_PropDataType - // not to be used in runtime communications! - "010": { - use: "FB", - name: "DPT_PropDataType", - desc: "Identifier Interface Object Property data type ", - }, + // 7.010 DPT_PropDataType + // not to be used in runtime communications! + '010': { + use: 'FB', + name: 'DPT_PropDataType', + desc: 'Identifier Interface Object Property data type ', + }, - // 7.011 - "011": { - use: "FB SAB", - name: "DPT_Length_mm", - desc: "Length in mm", - unit: "mm", - }, + // 7.011 + '011': { + use: 'FB SAB', + name: 'DPT_Length_mm', + desc: 'Length in mm', + unit: 'mm', + }, - // 7.012 - "012": { - use: "FB", - name: "DPT_UEICurrentmA", - desc: "bus power supply current (mA)", - unit: "mA", - }, + // 7.012 + '012': { + use: 'FB', + name: 'DPT_UEICurrentmA', + desc: 'bus power supply current (mA)', + unit: 'mA', + }, - // 7.013 - "013": { - use: "FB", - name: "DPT_Brightness", - desc: "interior brightness", - unit: "lux", - }, + // 7.013 + '013': { + use: 'FB', + name: 'DPT_Brightness', + desc: 'interior brightness', + unit: 'lux', + }, - // 7.600 - "600": { - use: "FB", - name: "DPT_Absolute_Colour_Temperature", - desc: "Absolute colour temperature", - unit: "K", - }, - }, -}; + // 7.600 + '600': { + use: 'FB', + name: 'DPT_Absolute_Colour_Temperature', + desc: 'Absolute colour temperature', + unit: 'K', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt8.ts b/src/dptlib/dpt8.ts index a123274..1122612 100644 --- a/src/dptlib/dpt8.ts +++ b/src/dptlib/dpt8.ts @@ -3,88 +3,88 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from "."; +import type { DatapointConfig } from '.' // // DPT8.*: 2-byte signed value // const config: DatapointConfig = { - id: "DPT8", - // DPT8 basetype info - basetype: { - bitlength: 16, - signedness: "signed", - valuetype: "basic", - range: [-32768, 32767], - desc: "16-bit signed value", - }, + id: 'DPT8', + // DPT8 basetype info + basetype: { + bitlength: 16, + signedness: 'signed', + valuetype: 'basic', + range: [-32768, 32767], + desc: '16-bit signed value', + }, - // DPT8 subtypes info - subtypes: { - // 8.001 pulses difference - "001": { - name: "DPT_Value_2_Count", - desc: "pulses", - unit: "pulses", - }, + // DPT8 subtypes info + subtypes: { + // 8.001 pulses difference + '001': { + name: 'DPT_Value_2_Count', + desc: 'pulses', + unit: 'pulses', + }, - // 8.002 time lag (ms) - "002": { - name: "DPT_DeltaTimeMsec", - desc: "time lag(ms)", - unit: "milliseconds", - }, + // 8.002 time lag (ms) + '002': { + name: 'DPT_DeltaTimeMsec', + desc: 'time lag(ms)', + unit: 'milliseconds', + }, - // 8.003 time lag (10ms) - "003": { - name: "DPT_DeltaTime10Msec", - desc: "time lag(10ms)", - unit: "centiseconds", - }, + // 8.003 time lag (10ms) + '003': { + name: 'DPT_DeltaTime10Msec', + desc: 'time lag(10ms)', + unit: 'centiseconds', + }, - // 8.004 time lag (100ms) - "004": { - name: "DPT_DeltaTime100Msec", - desc: "time lag(100ms)", - unit: "deciseconds", - }, + // 8.004 time lag (100ms) + '004': { + name: 'DPT_DeltaTime100Msec', + desc: 'time lag(100ms)', + unit: 'deciseconds', + }, - // 8.005 time lag (sec) - "005": { - name: "DPT_DeltaTimeSec", - desc: "time lag(s)", - unit: "seconds", - }, + // 8.005 time lag (sec) + '005': { + name: 'DPT_DeltaTimeSec', + desc: 'time lag(s)', + unit: 'seconds', + }, - // 8.006 time lag (min) - "006": { - name: "DPT_DeltaTimeMin", - desc: "time lag(min)", - unit: "minutes", - }, + // 8.006 time lag (min) + '006': { + name: 'DPT_DeltaTimeMin', + desc: 'time lag(min)', + unit: 'minutes', + }, - // 8.007 time lag (hour) - "007": { - name: "DPT_DeltaTimeHrs", - desc: "time lag(hrs)", - unit: "hours", - }, + // 8.007 time lag (hour) + '007': { + name: 'DPT_DeltaTimeHrs', + desc: 'time lag(hrs)', + unit: 'hours', + }, - // 8.010 percentage difference (%) - "010": { - name: "DPT_Percent_V16", - desc: "percentage difference", - unit: "%", - }, + // 8.010 percentage difference (%) + '010': { + name: 'DPT_Percent_V16', + desc: 'percentage difference', + unit: '%', + }, - // 8.011 rotation angle (deg) - "011": { - name: "DPT_RotationAngle", - desc: "angle(degrees)", - unit: "°", - }, - }, -}; + // 8.011 rotation angle (deg) + '011': { + name: 'DPT_RotationAngle', + desc: 'angle(degrees)', + unit: '°', + }, + }, +} -export default config; +export default config diff --git a/src/dptlib/dpt9.ts b/src/dptlib/dpt9.ts index 166824b..9273fb6 100644 --- a/src/dptlib/dpt9.ts +++ b/src/dptlib/dpt9.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from "../KnxLog"; -import { DatapointConfig } from "."; +import logger from '../KnxLog' +import type { DatapointConfig } from '.' -const log = logger.get(); +const log = logger.get() // // DPT9.*: 2-byte floating point value @@ -14,224 +14,225 @@ const log = logger.get(); // kudos to http://croquetweak.blogspot.gr/2014/08/deconstructing-floats-frexp-and-ldexp.html const ldexp = (mantissa, exponent) => - exponent > 1023 // avoid multiplying by infinity - ? mantissa * Math.pow(2, 1023) * Math.pow(2, exponent - 1023) - : exponent < -1074 // avoid multiplying by zero - ? mantissa * Math.pow(2, -1074) * Math.pow(2, exponent + 1074) - : mantissa * Math.pow(2, exponent); + // eslint-disable-next-line no-nested-ternary + exponent > 1023 // avoid multiplying by infinity + ? mantissa * 2 ** 1023 * 2 ** (exponent - 1023) + : exponent < -1074 // avoid multiplying by zero + ? mantissa * 2 ** -1074 * 2 ** (exponent + 1074) + : mantissa * 2 ** exponent const frexp = (value) => { - if (value === 0) return [0, 0]; - const data = new DataView(new ArrayBuffer(8)); - data.setFloat64(0, value); - let bits = (data.getUint32(0) >>> 20) & 0x7ff; - if (bits === 0) { - data.setFloat64(0, value * Math.pow(2, 64)); - bits = ((data.getUint32(0) >>> 20) & 0x7ff) - 64; - } - const exponent = bits - 1022; - const mantissa = ldexp(value, -exponent); - return [mantissa, exponent]; -}; + if (value === 0) return [0, 0] + const data = new DataView(new ArrayBuffer(8)) + data.setFloat64(0, value) + let bits = (data.getUint32(0) >>> 20) & 0x7ff + if (bits === 0) { + data.setFloat64(0, value * 2 ** 64) + bits = ((data.getUint32(0) >>> 20) & 0x7ff) - 64 + } + const exponent = bits - 1022 + const mantissa = ldexp(value, -exponent) + return [mantissa, exponent] +} const config: DatapointConfig = { - id: "DPT9", - formatAPDU: (value) => { - if (!isFinite(value)) - return log.warn("DPT9: cannot write non-numeric or undefined value"); - - const arr = frexp(value); - const [mantissa, exponent] = arr; - // find the minimum exponent that will upsize the normalized mantissa (0,5 to 1 range) - // in order to fit in 11 bits ([-2048, 2047]) - let max_mantissa = 0; - let e: number - for (e = exponent; e >= -15; e--) { - max_mantissa = ldexp(100 * mantissa, e); - if (max_mantissa > -2048 && max_mantissa < 2047) break; - } - const sign = mantissa < 0 ? 1 : 0; - const mant = mantissa < 0 ? ~(max_mantissa ^ 2047) : max_mantissa; - const exp = exponent - e; - // yucks - return Buffer.from([(sign << 7) + (exp << 3) + (mant >> 8), mant % 256]); - }, - - fromBuffer: (buf) => { - if (buf.length != 2) - return log.warn( - "DPT9.fromBuffer: buf should be 2 bytes long (got %d bytes)", - buf.length - ); - - const sign = buf[0] >> 7; - const exponent = (buf[0] & 0b01111000) >> 3; - let mantissa = 256 * (buf[0] & 0b00000111) + buf[1]; - if (sign) mantissa = ~(mantissa ^ 2047); - return parseFloat(ldexp(0.01 * mantissa, exponent).toPrecision(15)); - }, - - // DPT9 basetype info - basetype: { - bitlength: 16, - valuetype: "basic", - desc: "16-bit floating point value", - }, - - // DPT9 subtypes - subtypes: { - // 9.001 temperature (oC) - "001": { - name: "DPT_Value_Temp", - desc: "temperature", - unit: "°C", - range: [-273, 670760], - }, - - // 9.002 temperature difference (oC) - "002": { - name: "DPT_Value_Tempd", - desc: "temperature difference", - unit: "°C", - range: [-670760, 670760], - }, - - // 9.003 kelvin/hour (K/h) - "003": { - name: "DPT_Value_Tempa", - desc: "kelvin/hour", - unit: "°K/h", - range: [-670760, 670760], - }, - - // 9.004 lux (Lux) - "004": { - name: "DPT_Value_Lux", - desc: "lux", - unit: "lux", - range: [0, 670760], - }, - - // 9.005 speed (m/s) - "005": { - name: "DPT_Value_Wsp", - desc: "wind speed", - unit: "m/s", - range: [0, 670760], - }, - - // 9.006 pressure (Pa) - "006": { - name: "DPT_Value_Pres", - desc: "pressure", - unit: "Pa", - range: [0, 670760], - }, - - // 9.007 humidity (%) - "007": { - name: "DPT_Value_Humidity", - desc: "humidity", - unit: "%", - range: [0, 670760], - }, - - // 9.008 parts/million (ppm) - "008": { - name: "DPT_Value_AirQuality", - desc: "air quality", - unit: "ppm", - range: [0, 670760], - }, - - // 9.010 time (s) - "010": { - name: "DPT_Value_Time1", - desc: "time(sec)", - unit: "s", - range: [-670760, 670760], - }, - - // 9.011 time (ms) - "011": { - name: "DPT_Value_Time2", - desc: "time(msec)", - unit: "ms", - range: [-670760, 670760], - }, - - // 9.020 voltage (mV) - "020": { - name: "DPT_Value_Volt", - desc: "voltage", - unit: "mV", - range: [-670760, 670760], - }, - - // 9.021 current (mA) - "021": { - name: "DPT_Value_Curr", - desc: "current", - unit: "mA", - range: [-670760, 670760], - }, - - // 9.022 power density (W/m2) - "022": { - name: "DPT_PowerDensity", - desc: "power density", - unit: "W/m²", - range: [-670760, 670760], - }, - - // 9.023 kelvin/percent (K/%) - "023": { - name: "DPT_KelvinPerPercent", - desc: "Kelvin / %", - unit: "K/%", - range: [-670760, 670760], - }, - - // 9.024 power (kW) - "024": { - name: "DPT_Power", - desc: "power (kW)", - unit: "kW", - range: [-670760, 670760], - }, - - // 9.025 volume flow (l/h) - "025": { - name: "DPT_Value_Volume_Flow", - desc: "volume flow", - unit: "l/h", - range: [-670760, 670760], - }, - - // 9.026 rain amount (l/m2) - "026": { - name: "DPT_Rain_Amount", - desc: "rain amount", - unit: "l/m²", - range: [-670760, 670760], - }, - - // 9.027 temperature (Fahrenheit) - "027": { - name: "DPT_Value_Temp_F", - desc: "temperature (F)", - unit: "°F", - range: [-459.6, 670760], - }, - - // 9.028 wind speed (km/h) - "028": { - name: "DPT_Value_Wsp_kmh", - desc: "wind speed (km/h)", - unit: "km/h", - range: [0, 670760], - }, - }, -}; - -export default config; + id: 'DPT9', + formatAPDU: (value) => { + if (!isFinite(value)) + return log.warn('DPT9: cannot write non-numeric or undefined value') + + const arr = frexp(value) + const [mantissa, exponent] = arr + // find the minimum exponent that will upsize the normalized mantissa (0,5 to 1 range) + // in order to fit in 11 bits ([-2048, 2047]) + let max_mantissa = 0 + let e: number + for (e = exponent; e >= -15; e--) { + max_mantissa = ldexp(100 * mantissa, e) + if (max_mantissa > -2048 && max_mantissa < 2047) break + } + const sign = mantissa < 0 ? 1 : 0 + const mant = mantissa < 0 ? ~(max_mantissa ^ 2047) : max_mantissa + const exp = exponent - e + // yucks + return Buffer.from([(sign << 7) + (exp << 3) + (mant >> 8), mant % 256]) + }, + + fromBuffer: (buf) => { + if (buf.length !== 2) + return log.warn( + 'DPT9.fromBuffer: buf should be 2 bytes long (got %d bytes)', + buf.length, + ) + + const sign = buf[0] >> 7 + const exponent = (buf[0] & 0b01111000) >> 3 + let mantissa = 256 * (buf[0] & 0b00000111) + buf[1] + if (sign) mantissa = ~(mantissa ^ 2047) + return parseFloat(ldexp(0.01 * mantissa, exponent).toPrecision(15)) + }, + + // DPT9 basetype info + basetype: { + bitlength: 16, + valuetype: 'basic', + desc: '16-bit floating point value', + }, + + // DPT9 subtypes + subtypes: { + // 9.001 temperature (oC) + '001': { + name: 'DPT_Value_Temp', + desc: 'temperature', + unit: '°C', + range: [-273, 670760], + }, + + // 9.002 temperature difference (oC) + '002': { + name: 'DPT_Value_Tempd', + desc: 'temperature difference', + unit: '°C', + range: [-670760, 670760], + }, + + // 9.003 kelvin/hour (K/h) + '003': { + name: 'DPT_Value_Tempa', + desc: 'kelvin/hour', + unit: '°K/h', + range: [-670760, 670760], + }, + + // 9.004 lux (Lux) + '004': { + name: 'DPT_Value_Lux', + desc: 'lux', + unit: 'lux', + range: [0, 670760], + }, + + // 9.005 speed (m/s) + '005': { + name: 'DPT_Value_Wsp', + desc: 'wind speed', + unit: 'm/s', + range: [0, 670760], + }, + + // 9.006 pressure (Pa) + '006': { + name: 'DPT_Value_Pres', + desc: 'pressure', + unit: 'Pa', + range: [0, 670760], + }, + + // 9.007 humidity (%) + '007': { + name: 'DPT_Value_Humidity', + desc: 'humidity', + unit: '%', + range: [0, 670760], + }, + + // 9.008 parts/million (ppm) + '008': { + name: 'DPT_Value_AirQuality', + desc: 'air quality', + unit: 'ppm', + range: [0, 670760], + }, + + // 9.010 time (s) + '010': { + name: 'DPT_Value_Time1', + desc: 'time(sec)', + unit: 's', + range: [-670760, 670760], + }, + + // 9.011 time (ms) + '011': { + name: 'DPT_Value_Time2', + desc: 'time(msec)', + unit: 'ms', + range: [-670760, 670760], + }, + + // 9.020 voltage (mV) + '020': { + name: 'DPT_Value_Volt', + desc: 'voltage', + unit: 'mV', + range: [-670760, 670760], + }, + + // 9.021 current (mA) + '021': { + name: 'DPT_Value_Curr', + desc: 'current', + unit: 'mA', + range: [-670760, 670760], + }, + + // 9.022 power density (W/m2) + '022': { + name: 'DPT_PowerDensity', + desc: 'power density', + unit: 'W/m²', + range: [-670760, 670760], + }, + + // 9.023 kelvin/percent (K/%) + '023': { + name: 'DPT_KelvinPerPercent', + desc: 'Kelvin / %', + unit: 'K/%', + range: [-670760, 670760], + }, + + // 9.024 power (kW) + '024': { + name: 'DPT_Power', + desc: 'power (kW)', + unit: 'kW', + range: [-670760, 670760], + }, + + // 9.025 volume flow (l/h) + '025': { + name: 'DPT_Value_Volume_Flow', + desc: 'volume flow', + unit: 'l/h', + range: [-670760, 670760], + }, + + // 9.026 rain amount (l/m2) + '026': { + name: 'DPT_Rain_Amount', + desc: 'rain amount', + unit: 'l/m²', + range: [-670760, 670760], + }, + + // 9.027 temperature (Fahrenheit) + '027': { + name: 'DPT_Value_Temp_F', + desc: 'temperature (F)', + unit: '°F', + range: [-459.6, 670760], + }, + + // 9.028 wind speed (km/h) + '028': { + name: 'DPT_Value_Wsp_kmh', + desc: 'wind speed (km/h)', + unit: 'km/h', + range: [0, 670760], + }, + }, +} + +export default config diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts index d305b44..76d09b8 100644 --- a/src/dptlib/index.ts +++ b/src/dptlib/index.ts @@ -33,213 +33,212 @@ Unlimited string 8859_1 . DPT 24 DPT 24 List 3-byte value 3 Byte DPT 232 DPT 232 RGB[0,0,0]...[255,255,255] */ -import * as fs from "fs"; -import * as path from "path"; -import * as util from "util"; -import KnxLog from "../KnxLog"; - -import DPT1 from "./dpt1"; -import DPT2 from "./dpt2"; -import DPT3 from "./dpt3"; -import DPT4 from "./dpt4"; -import DPT5 from "./dpt5"; -import DPT6 from "./dpt6"; -import DPT7 from "./dpt7"; -import DPT8 from "./dpt8"; -import DPT9 from "./dpt9"; -import DPT10 from "./dpt10"; -import DPT11 from "./dpt11"; -import DPT12 from "./dpt12"; -import DPT13 from "./dpt13"; -import DPT14 from "./dpt14"; -import DPT15 from "./dpt15"; -import DPT16 from "./dpt16"; -import DPT17 from "./dpt17"; -import DPT18 from "./dpt18"; -import DPT19 from "./dpt19"; -import DPT20 from "./dpt20"; -import DPT21 from "./dpt21"; -import DPT232 from "./dpt232"; -import DPT237 from "./dpt237"; -import DPT238 from "./dpt238"; -import { Datagram } from "src/FSM"; - -const log = KnxLog.get(); +import * as fs from 'fs' +import * as path from 'path' +import * as util from 'util' +import KnxLog from '../KnxLog' + +import DPT1 from './dpt1' +import DPT2 from './dpt2' +import DPT3 from './dpt3' +import DPT4 from './dpt4' +import DPT5 from './dpt5' +import DPT6 from './dpt6' +import DPT7 from './dpt7' +import DPT8 from './dpt8' +import DPT9 from './dpt9' +import DPT10 from './dpt10' +import DPT11 from './dpt11' +import DPT12 from './dpt12' +import DPT13 from './dpt13' +import DPT14 from './dpt14' +import DPT15 from './dpt15' +import DPT16 from './dpt16' +import DPT17 from './dpt17' +import DPT18 from './dpt18' +import DPT19 from './dpt19' +import DPT20 from './dpt20' +import DPT21 from './dpt21' +import DPT232 from './dpt232' +import DPT237 from './dpt237' +import DPT238 from './dpt238' +import { Datagram } from 'src/FSM' +import { hasProp } from 'src/utils' + +const log = KnxLog.get() interface DatapointSubtype { - scalar_range?: [number, number]; - name: string; - use?: string; - desc: string; - force_encoding?: string; - unit?: string; - enc?: Record; - range?: [number, number] | [undefined, undefined]; + scalar_range?: [number, number] + name: string + use?: string + desc: string + force_encoding?: string + unit?: string + enc?: Record + range?: [number, number] | [undefined, undefined] } export interface DatapointConfig { - id: string; - subtypeid?: string; - basetype: { - bitlength: number; - signedness?: string; - range?: [number, number]; - valuetype: string; - desc?: string; - }; - subtype?: DatapointSubtype; - subtypes?: Record; - formatAPDU?: (value: any) => Buffer | void; - fromBuffer?: (buf: Buffer) => any; + id: string + subtypeid?: string + basetype: { + bitlength: number + signedness?: string + range?: [number, number] + valuetype: string + desc?: string + } + subtype?: DatapointSubtype + subtypes?: Record + formatAPDU?: (value: any) => Buffer | void + fromBuffer?: (buf: Buffer) => any } const dpts: Record = { - [DPT1.id]: DPT1, - [DPT2.id]: DPT2, - [DPT3.id]: DPT3, - [DPT4.id]: DPT4, - [DPT5.id]: DPT5, - [DPT6.id]: DPT6, - [DPT7.id]: DPT7, - [DPT8.id]: DPT8, - [DPT9.id]: DPT9, - [DPT10.id]: DPT10, - [DPT11.id]: DPT11, - [DPT12.id]: DPT12, - [DPT13.id]: DPT13, - [DPT14.id]: DPT14, - [DPT15.id]: DPT15, - [DPT16.id]: DPT16, - [DPT17.id]: DPT17, - [DPT18.id]: DPT18, - [DPT19.id]: DPT19, - [DPT20.id]: DPT20, - [DPT21.id]: DPT21, - [DPT232.id]: DPT232, - [DPT237.id]: DPT237, - [DPT238.id]: DPT238, -}; - -export function resolve(dptid: string | number) { - const m = dptid - .toString() - .toUpperCase() - .match(/^(?:DPT)?(\d+)(\.(\d+))?$/); - if (m === null) throw "Invalid DPT format: " + dptid; - - const dptkey = util.format("DPT%s", m[1]) - const dpt = dpts[dptkey]; - if (!dpt) throw "Unsupported DPT: " + dptid; - - const cloned_dpt = cloneDpt(dpt); - if (m[3]) { - cloned_dpt.subtypeid = m[3]; - cloned_dpt.subtype = cloned_dpt.subtypes[m[3]]; - } - - return cloned_dpt; + [DPT1.id]: DPT1, + [DPT2.id]: DPT2, + [DPT3.id]: DPT3, + [DPT4.id]: DPT4, + [DPT5.id]: DPT5, + [DPT6.id]: DPT6, + [DPT7.id]: DPT7, + [DPT8.id]: DPT8, + [DPT9.id]: DPT9, + [DPT10.id]: DPT10, + [DPT11.id]: DPT11, + [DPT12.id]: DPT12, + [DPT13.id]: DPT13, + [DPT14.id]: DPT14, + [DPT15.id]: DPT15, + [DPT16.id]: DPT16, + [DPT17.id]: DPT17, + [DPT18.id]: DPT18, + [DPT19.id]: DPT19, + [DPT20.id]: DPT20, + [DPT21.id]: DPT21, + [DPT232.id]: DPT232, + [DPT237.id]: DPT237, + [DPT238.id]: DPT238, +} + +export function resolve(dptid: string | number): DatapointConfig { + const m = dptid + .toString() + .toUpperCase() + .match(/^(?:DPT)?(\d+)(\.(\d+))?$/) + if (m === null) throw Error(`Invalid DPT format: ${dptid}`) + + const dptkey = util.format('DPT%s', m[1]) + const dpt = dpts[dptkey] + if (!dpt) throw Error(`Unsupported DPT: ${dptid}`) + + const cloned_dpt = cloneDpt(dpt) + if (m[3]) { + cloned_dpt.subtypeid = m[3] + cloned_dpt.subtype = cloned_dpt.subtypes[m[3]] + } + + return cloned_dpt } export function populateAPDU( - value: any, - apdu: Datagram["cemi"]["apdu"], - dptid?: number | string + value: any, + apdu: Datagram['cemi']['apdu'], + dptid?: number | string, ) { - const dpt = resolve(dptid || "DPT1"); - const nbytes = Math.ceil(dpt.basetype.bitlength / 8); - apdu.data = Buffer.alloc(nbytes); - apdu.bitlength = (dpt.basetype && dpt.basetype.bitlength) || 1; - let tgtvalue = value; - - if (typeof dpt.formatAPDU == "function") { - apdu.data = dpt.formatAPDU(value); - return apdu; - } - - if (!isFinite(value)) - throw util.format("Invalid value, expected a %s", dpt.basetype.desc); - - const [r_min, r_max] = dpt.basetype.hasOwnProperty("range") - ? dpt.basetype.range - : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; - - if ( - dpt.hasOwnProperty("subtype") && - dpt.subtype.hasOwnProperty("scalar_range") - ) { - const [s_min, s_max] = dpt.subtype.scalar_range; - if (value < s_min || value > s_max) { - log.trace( - "Value %j(%s) out of scalar range(%j) for %s", - value, - typeof value, - dpt.subtype.scalar_range, - dpt.id - ); - } else { - const a = (s_max - s_min) / (r_max - r_min); - const b = s_min - r_min; - tgtvalue = Math.round((value - b) / a); - } - } else if (value < r_min || value > r_max) { - log.trace( - "Value %j(%s) out of bounds(%j) for %s.%s", - value, - typeof value, - dpt.subtype.scalar_range, - dpt.id, - dpt.subtypeid - ); - } - - if ( - dpt.basetype.hasOwnProperty("signedness") && - dpt.basetype.signedness == "signed" - ) { - apdu.data.writeIntBE(tgtvalue, 0, nbytes); - } else { - apdu.data.writeUIntBE(tgtvalue, 0, nbytes); - } + const dpt = resolve(dptid || 'DPT1') + const nbytes = Math.ceil(dpt.basetype.bitlength / 8) + apdu.data = Buffer.alloc(nbytes) + apdu.bitlength = (dpt.basetype && dpt.basetype.bitlength) || 1 + let tgtvalue = value + + if (typeof dpt.formatAPDU === 'function') { + apdu.data = dpt.formatAPDU(value) + return apdu + } + + if (!isFinite(value)) + throw Error( + util.format('Invalid value, expected a %s', dpt.basetype.desc), + ) + + const [r_min, r_max] = hasProp(dpt.basetype, 'range') + ? dpt.basetype.range + : [0, 2 ** dpt.basetype.bitlength - 1] + + if (hasProp(dpt, 'subtype') && hasProp(dpt.subtype, 'scalar_range')) { + const [s_min, s_max] = dpt.subtype.scalar_range + if (value < s_min || value > s_max) { + log.trace( + 'Value %j(%s) out of scalar range(%j) for %s', + value, + typeof value, + dpt.subtype.scalar_range, + dpt.id, + ) + } else { + const a = (s_max - s_min) / (r_max - r_min) + const b = s_min - r_min + tgtvalue = Math.round((value - b) / a) + } + } else if (value < r_min || value > r_max) { + log.trace( + 'Value %j(%s) out of bounds(%j) for %s.%s', + value, + typeof value, + dpt.subtype.scalar_range, + dpt.id, + dpt.subtypeid, + ) + } + + if ( + hasProp(dpt.basetype, 'signedness') && + dpt.basetype.signedness === 'signed' + ) { + apdu.data.writeIntBE(tgtvalue, 0, nbytes) + } else { + apdu.data.writeUIntBE(tgtvalue, 0, nbytes) + } } export function fromBuffer(buf: Buffer, dpt: DatapointConfig) { - if (!dpt) throw util.format("DPT %s not found", dpt); - - if (typeof dpt.fromBuffer == "function") { - return dpt.fromBuffer(buf); - } - - if (buf.length > 6) { - throw "cannot handle unsigned integers more then 6 bytes in length"; - } - - let value = 0; - if ( - dpt.basetype.hasOwnProperty("signedness") && - dpt.basetype.signedness == "signed" - ) - value = buf.readIntBE(0, buf.length); - else value = buf.readUIntBE(0, buf.length); - - if ( - dpt.hasOwnProperty("subtype") && - dpt.subtype.hasOwnProperty("scalar_range") - ) { - const [r_min, r_max] = dpt.basetype.hasOwnProperty("range") - ? dpt.basetype.range - : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; - const [s_min, s_max] = dpt.subtype.scalar_range; - const a = (s_max - s_min) / (r_max - r_min); - const b = s_min - r_min; - value = Math.round(a * value + b); - } - - return value; + if (!dpt) throw Error(util.format('DPT %s not found', dpt)) + + if (typeof dpt.fromBuffer === 'function') { + return dpt.fromBuffer(buf) + } + + if (buf.length > 6) { + throw Error( + 'cannot handle unsigned integers more then 6 bytes in length', + ) + } + + let value = 0 + if ( + hasProp(dpt.basetype, 'signedness') && + dpt.basetype.signedness === 'signed' + ) + value = buf.readIntBE(0, buf.length) + else value = buf.readUIntBE(0, buf.length) + + if (hasProp(dpt, 'subtype') && hasProp(dpt.subtype, 'scalar_range')) { + const [r_min, r_max] = hasProp(dpt.subtype, 'range') + ? dpt.basetype.range + : [0, 2 ** dpt.basetype.bitlength - 1] + const [s_min, s_max] = dpt.subtype.scalar_range + const a = (s_max - s_min) / (r_max - r_min) + const b = s_min - r_min + value = Math.round(a * value + b) + } + + return value } const cloneDpt = (d: DatapointConfig) => { - const { fromBuffer, formatAPDU } = d; - return { ...JSON.parse(JSON.stringify(d)), fromBuffer, formatAPDU }; -}; + const { fromBuffer: fb, formatAPDU: fa } = d + return { ...JSON.parse(JSON.stringify(d)), fromBuffer: fb, formatAPDU: fa } +} -export default dpts; +export default dpts diff --git a/src/index.ts b/src/index.ts index 69c15c7..4e3ab1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,27 @@ /** -* knx.ts - a KNX protocol stack in pure Typescript -* (C) 2016-2017 Elias Karakoulakis -*/ + * knx.ts - a KNX protocol stack in pure Typescript + * (C) 2016-2017 Elias Karakoulakis + */ -import { format } from 'util'; -import { logger } from 'log-driver'; +import { format } from 'util' +import { logger } from 'log-driver' -import Connection from './FSM'; -import Datapoint from './Datapoint'; -import Devices from './devices'; -import Log from './KnxLog'; +import Connection from './FSM' +import Datapoint from './Datapoint' +import Devices from './devices' +import Log from './KnxLog' -const pkgJson = require('../package.json'); +// do not use import here or package.json would be loaded twice +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkgJson = require('../package.json') -logger.info(format('Loading %s: %s, version: %s', -pkgJson.name, pkgJson.description, pkgJson.version)); +logger.info( + format( + 'Loading %s: %s, version: %s', + pkgJson.name, + pkgJson.description, + pkgJson.version, + ), +) -export { Connection, Datapoint, Devices, Log }; \ No newline at end of file +export { Connection, Datapoint, Devices, Log } diff --git a/src/types/binary-protocol.d.ts b/src/types/binary-protocol.d.ts index 2adecc9..d170d25 100644 --- a/src/types/binary-protocol.d.ts +++ b/src/types/binary-protocol.d.ts @@ -1,55 +1,55 @@ -import { Duplex } from "stream"; +import { Duplex } from 'stream' -declare module "binary-protocol" { - interface ProtocolConfig { - read(propertyName: string): void; - write(value: any): void; - } +declare module 'binary-protocol' { + interface ProtocolConfig { + read(propertyName: string): void + write(value: any): void + } - interface Reader { - define(name: string, config: ProtocolConfig): this; - clone(): this; - demand(howMany: number): this; - tap(callback: (value: any) => void): this; - enqueue(name: string, arg1: any, arg2: any): this; - prepend(fn: (arg: any) => void, arg1: any): this; - pushStack(item: any): this; - popStack(property: string, fn: (value: any) => void): this; - collect(property: string, fn: (value: any) => void): this; - loop(property: string, fn: (value: any) => void): this; - end(fn: () => void): this; - finally(fn: () => void): this; - reset(): this; - raw(property: string, fn: (value: any) => void): this; - next(chunk?: any): any; - process(): any; - createLooper(property: string, fn: (value: any) => void): this; - [key: string]: any; // method created with `define` - } + interface Reader { + define(name: string, config: ProtocolConfig): this + clone(): this + demand(howMany: number): this + tap(callback: (value: any) => void): this + enqueue(name: string, arg1: any, arg2: any): this + prepend(fn: (arg: any) => void, arg1: any): this + pushStack(item: any): this + popStack(property: string, fn: (value: any) => void): this + collect(property: string, fn: (value: any) => void): this + loop(property: string, fn: (value: any) => void): this + end(fn: () => void): this + finally(fn: () => void): this + reset(): this + raw(property: string, fn: (value: any) => void): this + next(chunk?: any): any + process(): any + createLooper(property: string, fn: (value: any) => void): this + [key: string]: any // method created with `define` + } - interface Writer { - buffer: Buffer; - define(name: string, config: ProtocolConfig): this; - clone(): this; - allocate(howMany: number): this; - raw(buffer: Buffer): this; - forward(howMany: number): this; - tap(callback: (value: any) => void): this; - [key: string]: any; // method created with `define` - } + interface Writer { + buffer: Buffer + define(name: string, config: ProtocolConfig): this + clone(): this + allocate(howMany: number): this + raw(buffer: Buffer): this + forward(howMany: number): this + tap(callback: (value: any) => void): this + [key: string]: any // method created with `define` + } - interface Commander { - define(name: string, config: ProtocolConfig): this; - clone(): this; - createReadStream(options: any): Reader; - createWriteStream(options: any): Writer; - } + interface Commander { + define(name: string, config: ProtocolConfig): this + clone(): this + createReadStream(options: any): Reader + createWriteStream(options: any): Writer + } - export default class BinaryProtocol { - define(name: string, config: ProtocolConfig): this; - createReader(buffer: Buffer, offset?: number): Reader; - createWriter(buffer?: Buffer, offset?: number): Writer; - createCommander(duplex: Duplex): Commander; - [key: string]: any; // method created with `define` - } + export default class BinaryProtocol { + define(name: string, config: ProtocolConfig): this + createReader(buffer: Buffer, offset?: number): Reader + createWriter(buffer?: Buffer, offset?: number): Writer + createCommander(duplex: Duplex): Commander + [key: string]: any // method created with `define` + } } diff --git a/src/types/log-driver.d.ts b/src/types/log-driver.d.ts index 0bd545e..57b2336 100644 --- a/src/types/log-driver.d.ts +++ b/src/types/log-driver.d.ts @@ -1,21 +1,21 @@ -declare module "log-driver" { - export type LogLevel = "trace" | "debug" | "info" | "warn" | "error"; +declare module 'log-driver' { + export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' - export interface LogDriverOptions { - level: LogLevel; - format: (level: LogLevel, msg: string, ...args: any[]) => string; - } + export interface LogDriverOptions { + level: LogLevel + format: (level: LogLevel, msg: string, ...args: any[]) => string + } - export interface Logger { - trace: (...args: any[]) => void; - debug: (...args: any[]) => void; - info: (...args: any[]) => void; - warn: (...args: any[]) => void; - error: (...args: any[]) => void; - format: LogDriverOptions["format"]; - } + export interface Logger { + trace: (...args: any[]) => void + debug: (...args: any[]) => void + info: (...args: any[]) => void + warn: (...args: any[]) => void + error: (...args: any[]) => void + format: LogDriverOptions['format'] + } - export default function factory(options: LogDriverOptions): Logger; + export default function factory(options: LogDriverOptions): Logger - export const logger: Logger; + export const logger: Logger } diff --git a/src/types/machina.d.ts b/src/types/machina.d.ts index 1d035ef..acac96b 100644 --- a/src/types/machina.d.ts +++ b/src/types/machina.d.ts @@ -1,55 +1,55 @@ -import EventEmitter from "events"; - -declare module "machina" { - export interface States { - /** It's the "catch-all" handler which, if provided, will match any input in that state that's not explicitly matched by name */ - "*"?: () => void; - - _onEnter?: () => void; - - timeout?: string; - - _reset?: string; - - _onExit?: () => void; - - [key: string]: { - [key: string]: () => void; - }; - } - - export interface Options { - initialState: string; - states: Record; - namespace?: string; - initialize?: () => void; - } - - export class BehavioralFsm extends EventEmitter { - initialState: string; - namespace: string; - states: any; // Record; - state: string; - - static extend( - protoProps: Partial, - staticProps?: any - ): typeof BehavioralFsm; - compositeState(client: BehavioralFsm): any; - clearQueue(client: BehavioralFsm, name?: string): void; - handle(client: BehavioralFsm, ...args: any[]): any; - transition(client: BehavioralFsm, newState: string): void; - initialize(...args: any): void; - } - - export class Fsm extends BehavioralFsm { - static extend( - protoProps: Partial, - staticProps?: any - ): typeof Fsm; - compositeState(): any; - clearQueue(name?: string): void; - handle(...args: any[]): any; - transition(newState: string): void; - } +import EventEmitter from 'events' + +declare module 'machina' { + export interface States { + /** It's the "catch-all" handler which, if provided, will match any input in that state that's not explicitly matched by name */ + '*'?: () => void + + _onEnter?: () => void + + timeout?: string + + _reset?: string + + _onExit?: () => void + + [key: string]: { + [key: string]: () => void + } + } + + export interface Options { + initialState: string + states: Record + namespace?: string + initialize?: () => void + } + + export class BehavioralFsm extends EventEmitter { + initialState: string + + namespace: string + + states: any // Record; + + state: string + + static extend( + protoProps: Partial, + staticProps?: any, + ): typeof BehavioralFsm + compositeState(client: BehavioralFsm): any + clearQueue(client: BehavioralFsm, name?: string): void + handle(client: BehavioralFsm, ...args: any[]): any + transition(client: BehavioralFsm, newState: string): void + initialize(...args: any): void + } + + export class Fsm extends BehavioralFsm { + static extend(protoProps: Partial, staticProps?: any): typeof Fsm + compositeState(): any + clearQueue(name?: string): void + handle(...args: any[]): any + transition(newState: string): void + } } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..0b2b7da --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line import/prefer-default-export +export function hasProp(obj: any, prop: string): boolean { + return Object.prototype.hasOwnProperty.call(obj, prop) +} diff --git a/test/connection/test-connect-routing.ts b/test/connection/test-connect-routing.ts index 2959a93..22001a8 100644 --- a/test/connection/test-connect-routing.ts +++ b/test/connection/test-connect-routing.ts @@ -1,36 +1,36 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -Error.stackTraceLimit = Infinity; +import { Connection } from '../../src' +import test from 'tape' -import { Connection } from '../../src'; -import test from 'tape'; +Error.stackTraceLimit = Infinity // -test('KNX connect routing', function(t) { - var connection = new Connection({ - loglevel: 'trace', - handlers: { - connected: function() { - console.log('----------'); - console.log('Connected!'); - console.log('----------'); - t.pass('connected in routing mode'); - t.end(); - process.exit(0); - }, - error: function() { - t.fail('error connecting'); - t.end(); - process.exit(1); - } - } - }); -}); +test('KNX connect routing', function (t) { + const connection = new Connection({ + loglevel: 'trace', + handlers: { + connected() { + console.log('----------') + console.log('Connected!') + console.log('----------') + t.pass('connected in routing mode') + t.end() + process.exit(0) + }, + error() { + t.fail('error connecting') + t.end() + process.exit(1) + }, + }, + }) +}) -setTimeout(function() { - console.log('Exiting with timeout...'); - process.exit(2); -}, 1000); +setTimeout(function () { + console.log('Exiting with timeout...') + process.exit(2) +}, 1000) diff --git a/test/dptlib/commontest.ts b/test/dptlib/commontest.ts index a58b21a..d41295f 100644 --- a/test/dptlib/commontest.ts +++ b/test/dptlib/commontest.ts @@ -1,11 +1,12 @@ +/* eslint-disable no-prototype-builtins */ /** * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import test, { Test } from "tape"; -import DPTLib, { fromBuffer, populateAPDU, resolve } from "../../src/dptlib"; -import { Datagram } from "../../src/FSM"; +import test, { Test } from 'tape' +import DPTLib, { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' +import { Datagram } from '../../src/FSM' /* common DPT unit test. Tries to - 1. marshal a JS value into an apdu.data (Buffer) and compare it output to the expected value @@ -15,59 +16,60 @@ import { Datagram } from "../../src/FSM"; // marshalling test (JS value to APDU) function marshalTest(t: Test, dptid: string | number, jsval: any, apdu: any) { - const marshalled = {} as Datagram["cemi"]["apdu"]; - populateAPDU(jsval, marshalled, dptid); - // console.log('%j --> %j', apdu.constructor.name, marshalled.data) - t.deepEqual( - marshalled.data, - apdu, - `${dptid}.populateAPDU(${jsval}:${typeof jsval}) should be marshalled as \"0x${apdu.toString( - "hex" - )}\", got: \"0x${marshalled.data.toString("hex")}\"` - ); - return marshalled.data; + const marshalled = {} as Datagram['cemi']['apdu'] + populateAPDU(jsval, marshalled, dptid) + // console.log('%j --> %j', apdu.constructor.name, marshalled.data) + t.deepEqual( + marshalled.data, + apdu, + `${dptid}.populateAPDU(${jsval}:${typeof jsval}) should be marshalled as "0x${apdu.toString( + 'hex', + )}", got: "0x${marshalled.data.toString('hex')}"`, + ) + return marshalled.data } function unmarshalTest(t, dptid, jsval, data) { - var dpt = resolve(dptid); - var unmarshalled = fromBuffer(data, dpt); - //console.log('%s: %j --> %j', dpt.id, rhs, converted); - var msg = `${dptid}.fromBuffer(${JSON.stringify( - data - )}) should unmarshall to ${JSON.stringify(jsval)}, got: ${JSON.stringify( - unmarshalled - )}`; - switch (typeof jsval) { - case "object": - t.deepEqual(unmarshalled, jsval, msg); - break; - case "number": - t.equal(unmarshalled, jsval, msg); - break; - default: - t.ok(unmarshalled == jsval, msg); - } + const dpt = resolve(dptid) + const unmarshalled = fromBuffer(data, dpt) + // console.log('%s: %j --> %j', dpt.id, rhs, converted); + const msg = `${dptid}.fromBuffer(${JSON.stringify( + data, + )}) should unmarshall to ${JSON.stringify(jsval)}, got: ${JSON.stringify( + unmarshalled, + )}` + switch (typeof jsval) { + case 'object': + t.deepEqual(unmarshalled, jsval, msg) + break + case 'number': + t.equal(unmarshalled, jsval, msg) + break + default: + t.ok(unmarshalled === jsval, msg) + } } +// eslint-disable-next-line import/prefer-default-export export function run( - dptid: number | string, - tests: { apdu_data: number[]; jsval: any }[] + dptid: number | string, + tests: { apdu_data: number[]; jsval: any }[], ) { - var dpt = resolve(dptid); - var desc = - (dpt.hasOwnProperty("subtype") && dpt.subtype.desc) || dpt.basetype.desc; - test(`${dptid}: ${desc}`, function (t) { - Object.keys(tests).forEach(function (key) { - var apdu = Buffer.from(tests[key].apdu_data); - var jsval = tests[key].jsval; - //console.log(dptid + ': apdu=%j jsval=%j', apdu, jsval); - // 1. marshalling test (JS value to APDU) - var marshalled_data = marshalTest(t, dptid, jsval, apdu); - // 2. unmarshal from APDU produced by step 1 - unmarshalTest(t, dptid, jsval, marshalled_data); - // 3. unmarshal from test APDU - unmarshalTest(t, dptid, jsval, apdu); - }); - t.end(); - }); + const dpt = resolve(dptid) + const desc = + (dpt.hasOwnProperty('subtype') && dpt.subtype.desc) || dpt.basetype.desc + test(`${dptid}: ${desc}`, function (t) { + Object.keys(tests).forEach(function (key) { + const apdu = Buffer.from(tests[key].apdu_data) + const jsval = tests[key].jsval + // console.log(dptid + ': apdu=%j jsval=%j', apdu, jsval); + // 1. marshalling test (JS value to APDU) + const marshalled_data = marshalTest(t, dptid, jsval, apdu) + // 2. unmarshal from APDU produced by step 1 + unmarshalTest(t, dptid, jsval, marshalled_data) + // 3. unmarshal from test APDU + unmarshalTest(t, dptid, jsval, apdu) + }) + t.end() + }) } diff --git a/test/dptlib/test-dpt.ts b/test/dptlib/test-dpt.ts index 6ff86a8..28639ed 100644 --- a/test/dptlib/test-dpt.ts +++ b/test/dptlib/test-dpt.ts @@ -1,59 +1,83 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import test from "tape"; -import { resolve } from "../../src/dptlib"; +import test from 'tape' +import { resolve } from '../../src/dptlib' -test('resolve', function(t) { - t.throws(() => { - resolve('invalid input'); - }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); +test('resolve', function (t) { + t.throws( + () => { + resolve('invalid input') + }, + /Invalid DPT format: .*/, + 'Invalid format of a DPT', + ) - t.throws(() => { - resolve({dpt: 9} as any); - }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); + t.throws( + () => { + resolve({ dpt: 9 } as any) + }, + /Invalid DPT format: .*/, + 'Invalid format of a DPT', + ) - t.throws(() => { - resolve([9,9] as any); - }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); + t.throws( + () => { + resolve([9, 9] as any) + }, + /Invalid DPT format: .*/, + 'Invalid format of a DPT', + ) - t.throws(() => { - resolve('29.010'); - }, /Unsupported DPT: .*/, 'Unsupported/unknown DPT'); + t.throws( + () => { + resolve('29.010') + }, + /Unsupported DPT: .*/, + 'Unsupported/unknown DPT', + ) - t.throws(() => { - resolve(29); - }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT'); + t.throws( + () => { + resolve(29) + }, + /Unsupported DPT: .*/, + 'Unsupported/unknown Int DPT', + ) - t.throws(() => { - resolve([29] as any); - }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT'); + t.throws( + () => { + resolve([29] as any) + }, + /Unsupported DPT: .*/, + 'Unsupported/unknown Int DPT', + ) - var d0 = resolve(1) - t.equal(d0.id, 'DPT1') - t.equal(d0.subtypeid, undefined) + const d0 = resolve(1) + t.equal(d0.id, 'DPT1') + t.equal(d0.subtypeid, undefined) - var d1 = resolve('DPT9') - t.equal(d1.id, 'DPT9') - t.equal(d1.subtypeid, undefined) + const d1 = resolve('DPT9') + t.equal(d1.id, 'DPT9') + t.equal(d1.subtypeid, undefined) - var d2 = resolve('DPT1.002') - t.equal(d2.id, 'DPT1') - t.equal(d2.subtypeid, '002') + const d2 = resolve('DPT1.002') + t.equal(d2.id, 'DPT1') + t.equal(d2.subtypeid, '002') - var d3 = resolve('DPT1.001') - t.equal(d3.id, 'DPT1') - t.equal(d3.subtypeid, '001') + const d3 = resolve('DPT1.001') + t.equal(d3.id, 'DPT1') + t.equal(d3.subtypeid, '001') - // Check that dpts are not destroyed by subsequent calls to resolve - t.equal(d2.id, 'DPT1') - t.equal(d2.subtypeid, '002') + // Check that dpts are not destroyed by subsequent calls to resolve + t.equal(d2.id, 'DPT1') + t.equal(d2.subtypeid, '002') - var d4 = resolve('1.002') - t.equal(d4.id, 'DPT1') - t.equal(d4.subtypeid, '002') + const d4 = resolve('1.002') + t.equal(d4.id, 'DPT1') + t.equal(d4.subtypeid, '002') - t.end() + t.end() }) diff --git a/test/dptlib/test-dpt1.ts b/test/dptlib/test-dpt1.ts index e175d40..e141c85 100644 --- a/test/dptlib/test-dpt1.ts +++ b/test/dptlib/test-dpt1.ts @@ -1,10 +1,10 @@ -import { run } from "./commontest"; +import { run } from './commontest' /** * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -run("DPT1", [ - { apdu_data: [0x00], jsval: false }, - { apdu_data: [0x01], jsval: true }, -]); +run('DPT1', [ + { apdu_data: [0x00], jsval: false }, + { apdu_data: [0x01], jsval: true }, +]) diff --git a/test/dptlib/test-dpt10.ts b/test/dptlib/test-dpt10.ts index 47f4e36..e25d415 100644 --- a/test/dptlib/test-dpt10.ts +++ b/test/dptlib/test-dpt10.ts @@ -1,55 +1,61 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import test from 'tape'; -import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib'; -import { Datagram } from 'src/FSM'; +import test from 'tape' +import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' +import { Datagram } from 'src/FSM' function timecompare(date1, sign, date2) { - var dow1 = date1.getDay(); - var hour1 = date1.getHours(); - var min1 = date1.getMinutes(); - var sec1 = date1.getSeconds(); - var dow2 = date2.getDay(); - var hour2 = date2.getHours(); - var min2 = date2.getMinutes(); - var sec2 = date2.getSeconds(); - if (sign === '===') { - if (dow1 == dow2 && hour1 === hour2 && min1 === min2 && sec1 === sec2) return true; - else return false; - } else if (sign === '>') { - if (dow1 > dow2) return true; - else if (dow1 == dow2 && hour1 > hour2) return true; - else if (dow1 == dow2 && hour1 === hour2 && min1 > min2) return true; - else if (dow1 == dow2 && hour1 === hour2 && min1 === min2 && sec1 > sec2) return true; - else return false; - } + const dow1 = date1.getDay() + const hour1 = date1.getHours() + const min1 = date1.getMinutes() + const sec1 = date1.getSeconds() + const dow2 = date2.getDay() + const hour2 = date2.getHours() + const min2 = date2.getMinutes() + const sec2 = date2.getSeconds() + if (sign === '===') { + if (dow1 === dow2 && hour1 === hour2 && min1 === min2 && sec1 === sec2) + return true + return false + } + if (sign === '>') { + if (dow1 > dow2) return true + if (dow1 === dow2 && hour1 > hour2) return true + if (dow1 === dow2 && hour1 === hour2 && min1 > min2) return true + if (dow1 === dow2 && hour1 === hour2 && min1 === min2 && sec1 > sec2) + return true + return false + } } -test('DPT10 time conversion', function(t) { +test('DPT10 time conversion', function (t) { + const tests = [ + ['DPT10', [(1 << 5) + 23, 15, 30], new Date('July 1, 2019 23:15:30')], // Monday + ['DPT10', [(3 << 5) + 14, 55, 11], new Date('July 10, 2019 14:55:11')], // Wednesday + ['DPT10', [(7 << 5) + 23, 15, 30], new Date('July 7, 2019 23:15:30')], // Sunday + ] + for (let i = 0; i < tests.length; i++) { + const dpt = resolve(tests[i][0] as string) + const buf = Buffer.from(tests[i][1] as number[]) + const val = tests[i][2] - var tests = [ - ['DPT10', [(1<<5)+23, 15, 30], new Date('July 1, 2019 23:15:30')], // Monday - ['DPT10', [(3<<5)+14, 55, 11], new Date('July 10, 2019 14:55:11')],// Wednesday - ['DPT10', [(7<<5)+23, 15, 30], new Date('July 7, 2019 23:15:30')] // Sunday - ]; - for (var i = 0; i < tests.length; i++) { - var dpt = resolve(tests[i][0] as string); - var buf = Buffer.from(tests[i][1] as number[]); - var val = tests[i][2]; + // unmarshalling test (raw data to value) + const converted = fromBuffer(buf, dpt) + t.ok( + timecompare(converted, '===', val), + `${tests[i][0]} fromBuffer value ${buf.toString('hex')} => expected ${val}, got ${converted}`, + ) - // unmarshalling test (raw data to value) - var converted = fromBuffer(buf, dpt); - t.ok(timecompare(converted, '===', val) , - `${tests[i][0]} fromBuffer value ${buf.toString('hex')} => expected ${val}, got ${converted}`); - - // marshalling test (value to raw data) - var apdu = {} as Datagram["cemi"]["apdu"]; - populateAPDU(val, apdu, 'dpt10'); - t.ok(Buffer.compare(buf, apdu.data) == 0, - `${tests[i][0]} formatAPDU value ${val} => expected ${buf.toString('hex')}, got ${apdu.data.toString('hex')}`); - } - t.end() + // marshalling test (value to raw data) + const apdu = {} as Datagram['cemi']['apdu'] + populateAPDU(val, apdu, 'dpt10') + t.ok( + Buffer.compare(buf, apdu.data) === 0, + `${tests[i][0]} formatAPDU value ${val} => expected ${buf.toString('hex')}, got ${apdu.data.toString('hex')}`, + ) + } + t.end() }) diff --git a/test/dptlib/test-dpt11.ts b/test/dptlib/test-dpt11.ts index 5273185..f801526 100644 --- a/test/dptlib/test-dpt11.ts +++ b/test/dptlib/test-dpt11.ts @@ -1,44 +1,46 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import test from 'tape'; -import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib'; -import { Datagram } from 'src/FSM'; +import test from 'tape' +import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' +import { Datagram } from 'src/FSM' function dateequals(d1: Date, d2: Date) { - var d = d1.getDate(); - var m = d1.getMonth(); - var y = d1.getFullYear(); - return (d == d2.getDate() && m == d2.getMonth() && y == d2.getFullYear()); + const d = d1.getDate() + const m = d1.getMonth() + const y = d1.getFullYear() + return d === d2.getDate() && m === d2.getMonth() && y === d2.getFullYear() } -test('DPT11 date conversion', function(t) { - var tests = [ - ['DPT11', [25, 12, 95], new Date('1995-12-25')], - ['DPT11', [0x19, 0x0C, 0x0F], new Date('2015-12-25')], - ['DPT11', [0x16, 0x0B, 0x10], new Date('2016-11-22')], - ['DPT11', [0x1B, 0x01, 0x13], new Date('2019-01-27')], - ['DPT11', [0x03, 0x02, 0x13], new Date('2019-02-03')] - ] - for (var i = 0; i < tests.length; i++) { - var dpt = resolve(tests[i][0] as string); - var buf = Buffer.from(tests[i][1] as number[]); - var val = tests[i][2] as Date; +test('DPT11 date conversion', function (t) { + const tests = [ + ['DPT11', [25, 12, 95], new Date('1995-12-25')], + ['DPT11', [0x19, 0x0c, 0x0f], new Date('2015-12-25')], + ['DPT11', [0x16, 0x0b, 0x10], new Date('2016-11-22')], + ['DPT11', [0x1b, 0x01, 0x13], new Date('2019-01-27')], + ['DPT11', [0x03, 0x02, 0x13], new Date('2019-02-03')], + ] + for (let i = 0; i < tests.length; i++) { + const dpt = resolve(tests[i][0] as string) + const buf = Buffer.from(tests[i][1] as number[]) + const val = tests[i][2] as Date - // unmarshalling test (raw data to value) - var converted = fromBuffer(buf, dpt); - t.ok(dateequals(val, converted), - `${tests[i][0]} fromBuffer value ${val} => ${JSON.stringify(converted)}` - ); + // unmarshalling test (raw data to value) + const converted = fromBuffer(buf, dpt) + t.ok( + dateequals(val, converted), + `${tests[i][0]} fromBuffer value ${val} => ${JSON.stringify(converted)}`, + ) - // marshalling test (value to raw data) - var apdu = {} as Datagram["cemi"]["apdu"]; - populateAPDU(val, apdu, 'dpt11'); - t.ok(Buffer.compare(buf, apdu.data) == 0, - `${tests[i][0]} formatAPDU value ${val} => ${JSON.stringify(converted)}` - ); - } - t.end() + // marshalling test (value to raw data) + const apdu = {} as Datagram['cemi']['apdu'] + populateAPDU(val, apdu, 'dpt11') + t.ok( + Buffer.compare(buf, apdu.data) === 0, + `${tests[i][0]} formatAPDU value ${val} => ${JSON.stringify(converted)}`, + ) + } + t.end() }) diff --git a/test/dptlib/test-dpt12.ts b/test/dptlib/test-dpt12.ts index d2ffd7f..d1d8c29 100644 --- a/test/dptlib/test-dpt12.ts +++ b/test/dptlib/test-dpt12.ts @@ -1,16 +1,16 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT12', [ - { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17}, - { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256}, - { apdu_data: [0x00, 0x00, 0x10, 0x01], jsval: 4097}, - { apdu_data: [0x00, 0x00, 0xff, 0xff], jsval: 65535}, - { apdu_data: [0x00, 0x01, 0x00, 0x00], jsval: 65536}, - { apdu_data: [0x07, 0x5b, 0xcd, 0x15], jsval: 123456789}, - { apdu_data: [0x49, 0x96, 0x02, 0xd2], jsval: 1234567890}, -]); + { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17 }, + { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256 }, + { apdu_data: [0x00, 0x00, 0x10, 0x01], jsval: 4097 }, + { apdu_data: [0x00, 0x00, 0xff, 0xff], jsval: 65535 }, + { apdu_data: [0x00, 0x01, 0x00, 0x00], jsval: 65536 }, + { apdu_data: [0x07, 0x5b, 0xcd, 0x15], jsval: 123456789 }, + { apdu_data: [0x49, 0x96, 0x02, 0xd2], jsval: 1234567890 }, +]) diff --git a/test/dptlib/test-dpt13.ts b/test/dptlib/test-dpt13.ts index f1386b8..7a002ad 100644 --- a/test/dptlib/test-dpt13.ts +++ b/test/dptlib/test-dpt13.ts @@ -1,17 +1,17 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT13', [ - { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17}, - { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256}, - { apdu_data: [0x00, 0x00, 0x10, 0x00], jsval: 4096}, - { apdu_data: [0x00, 0x01, 0x00, 0x00], jsval: 65536}, - { apdu_data: [0x7f, 0xff, 0xff, 0xff], jsval: 2147483647}, - { apdu_data: [0x80, 0x00, 0x00, 0x00], jsval: -2147483648}, - { apdu_data: [0x80, 0x00, 0x00, 0x01], jsval: -2147483647}, - { apdu_data: [0xff, 0xff, 0xff, 0xff], jsval: -1}, -]); + { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17 }, + { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256 }, + { apdu_data: [0x00, 0x00, 0x10, 0x00], jsval: 4096 }, + { apdu_data: [0x00, 0x01, 0x00, 0x00], jsval: 65536 }, + { apdu_data: [0x7f, 0xff, 0xff, 0xff], jsval: 2147483647 }, + { apdu_data: [0x80, 0x00, 0x00, 0x00], jsval: -2147483648 }, + { apdu_data: [0x80, 0x00, 0x00, 0x01], jsval: -2147483647 }, + { apdu_data: [0xff, 0xff, 0xff, 0xff], jsval: -1 }, +]) diff --git a/test/dptlib/test-dpt19.ts b/test/dptlib/test-dpt19.ts index 4c05a49..c38694c 100644 --- a/test/dptlib/test-dpt19.ts +++ b/test/dptlib/test-dpt19.ts @@ -1,48 +1,50 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ - -import test from 'tape'; -import DPTLib, { fromBuffer, populateAPDU, resolve } from '../../src/dptlib'; -import { Datagram } from 'src/FSM'; - -test('DPT19 datetime conversion', function(t) { - - var tests = ['1995-12-17T03:24:00', '1996-07-17T03:24:00']; - - Object.keys(tests).forEach(function(key) { - var date = new Date(tests[key]); - date.setMilliseconds(0); - - var day = (date.getDay() === 0) ? 7 : date.getDay(); - var buffer = Buffer.from([ - date.getFullYear() - 1900, // year with offset 1900 - date.getMonth() + 1, // month from 1 - 12 - date.getDate(), // day of month from 1 - 31 - (day << 5) + date.getHours(), // 3 bits: day of week (1-7), 5 bits: hour - date.getMinutes(), - date.getSeconds(), - 0, - 0 - ]); - - var name = 'DPT19'; - var dpt = resolve(name); - - // unmarshalling test (raw data to value) - var converted = fromBuffer(buffer, dpt); - t.equal(date.getTime(), converted.getTime(), - `${name} fromBuffer value ${JSON.stringify(buffer)} => ${converted}` - ); - - // marshalling test (value to raw data) - var apdu = {} as Datagram["cemi"]["apdu"]; - populateAPDU(date, apdu, name); - t.ok(Buffer.compare(buffer, apdu.data) === 0, - `${name} formatAPDU value ${date} => ${JSON.stringify(apdu)}` - ); - }); - - t.end(); -}); + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import test from 'tape' +import DPTLib, { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' +import { Datagram } from 'src/FSM' + +test('DPT19 datetime conversion', function (t) { + const tests = ['1995-12-17T03:24:00', '1996-07-17T03:24:00'] + + Object.keys(tests).forEach(function (key) { + const date = new Date(tests[key]) + date.setMilliseconds(0) + + const day = date.getDay() === 0 ? 7 : date.getDay() + const buffer = Buffer.from([ + date.getFullYear() - 1900, // year with offset 1900 + date.getMonth() + 1, // month from 1 - 12 + date.getDate(), // day of month from 1 - 31 + (day << 5) + date.getHours(), // 3 bits: day of week (1-7), 5 bits: hour + date.getMinutes(), + date.getSeconds(), + 0, + 0, + ]) + + const name = 'DPT19' + const dpt = resolve(name) + + // unmarshalling test (raw data to value) + const converted = fromBuffer(buffer, dpt) + t.equal( + date.getTime(), + converted.getTime(), + `${name} fromBuffer value ${JSON.stringify(buffer)} => ${converted}`, + ) + + // marshalling test (value to raw data) + const apdu = {} as Datagram['cemi']['apdu'] + populateAPDU(date, apdu, name) + t.ok( + Buffer.compare(buffer, apdu.data) === 0, + `${name} formatAPDU value ${date} => ${JSON.stringify(apdu)}`, + ) + }) + + t.end() +}) diff --git a/test/dptlib/test-dpt2.ts b/test/dptlib/test-dpt2.ts index 5b59664..06c7c01 100644 --- a/test/dptlib/test-dpt2.ts +++ b/test/dptlib/test-dpt2.ts @@ -1,13 +1,13 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT2', [ - { apdu_data: [0x00], jsval: {priority: 0 , data: 0}}, - { apdu_data: [0x01], jsval: {priority: 0 , data: 1}}, - { apdu_data: [0x02], jsval: {priority: 1 , data: 0}}, - { apdu_data: [0x03], jsval: {priority: 1 , data: 1}} -]); + { apdu_data: [0x00], jsval: { priority: 0, data: 0 } }, + { apdu_data: [0x01], jsval: { priority: 0, data: 1 } }, + { apdu_data: [0x02], jsval: { priority: 1, data: 0 } }, + { apdu_data: [0x03], jsval: { priority: 1, data: 1 } }, +]) diff --git a/test/dptlib/test-dpt21.ts b/test/dptlib/test-dpt21.ts index e8dee5c..2eb32b2 100644 --- a/test/dptlib/test-dpt21.ts +++ b/test/dptlib/test-dpt21.ts @@ -1,20 +1,101 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ -import { run } from "./commontest"; + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ +import { run } from './commontest' run('DPT21', [ - { apdu_data: [0x00], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, - { apdu_data: [0x01], jsval: {outofservice: 1, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, - { apdu_data: [0x03], jsval: {outofservice: 1, fault: 1, overridden: 0, inalarm: 0, alarmunack: 0}}, - { apdu_data: [0x05], jsval: {outofservice: 1, fault: 0, overridden: 1, inalarm: 0, alarmunack: 0}}, - { apdu_data: [0x08], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 1, alarmunack: 0}} -]); + { + apdu_data: [0x00], + jsval: { + outofservice: 0, + fault: 0, + overridden: 0, + inalarm: 0, + alarmunack: 0, + }, + }, + { + apdu_data: [0x01], + jsval: { + outofservice: 1, + fault: 0, + overridden: 0, + inalarm: 0, + alarmunack: 0, + }, + }, + { + apdu_data: [0x03], + jsval: { + outofservice: 1, + fault: 1, + overridden: 0, + inalarm: 0, + alarmunack: 0, + }, + }, + { + apdu_data: [0x05], + jsval: { + outofservice: 1, + fault: 0, + overridden: 1, + inalarm: 0, + alarmunack: 0, + }, + }, + { + apdu_data: [0x08], + jsval: { + outofservice: 0, + fault: 0, + overridden: 0, + inalarm: 1, + alarmunack: 0, + }, + }, +]) run('DPT21.001', [ - { apdu_data: [0x01], jsval: {outofservice: 1, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, - { apdu_data: [0x05], jsval: {outofservice: 1, fault: 0, overridden: 1, inalarm: 0, alarmunack: 0}}, - { apdu_data: [0x06], jsval: {outofservice: 0, fault: 1, overridden: 1, inalarm: 0, alarmunack: 0}}, - { apdu_data: [0x08], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 1, alarmunack: 0}} -]); + { + apdu_data: [0x01], + jsval: { + outofservice: 1, + fault: 0, + overridden: 0, + inalarm: 0, + alarmunack: 0, + }, + }, + { + apdu_data: [0x05], + jsval: { + outofservice: 1, + fault: 0, + overridden: 1, + inalarm: 0, + alarmunack: 0, + }, + }, + { + apdu_data: [0x06], + jsval: { + outofservice: 0, + fault: 1, + overridden: 1, + inalarm: 0, + alarmunack: 0, + }, + }, + { + apdu_data: [0x08], + jsval: { + outofservice: 0, + fault: 0, + overridden: 0, + inalarm: 1, + alarmunack: 0, + }, + }, +]) diff --git a/test/dptlib/test-dpt237.ts b/test/dptlib/test-dpt237.ts index c652bfe..f53ce70 100644 --- a/test/dptlib/test-dpt237.ts +++ b/test/dptlib/test-dpt237.ts @@ -1,29 +1,110 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ -import { run } from "./commontest"; + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ +import { run } from './commontest' run('DPT237', [ - { apdu_data: [0x00, 0x00], jsval: {address: 0, addresstype: 0, - readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, - { apdu_data: [0x00, 0x21], jsval: {address: 1, addresstype: 1, - readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, - { apdu_data: [0x00, 0x63], jsval: {address: 3, addresstype: 1, - readresponse: 1, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, - { apdu_data: [0x01, 0x05], jsval: {address: 5, addresstype: 0, - readresponse: 0, lampfailure: 0, ballastfailure: 1, convertorerror: 0}}, - { apdu_data: [0x02, 0x08], jsval: {address: 8, addresstype: 0, - readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 1}} -]); + { + apdu_data: [0x00, 0x00], + jsval: { + address: 0, + addresstype: 0, + readresponse: 0, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 0, + }, + }, + { + apdu_data: [0x00, 0x21], + jsval: { + address: 1, + addresstype: 1, + readresponse: 0, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 0, + }, + }, + { + apdu_data: [0x00, 0x63], + jsval: { + address: 3, + addresstype: 1, + readresponse: 1, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 0, + }, + }, + { + apdu_data: [0x01, 0x05], + jsval: { + address: 5, + addresstype: 0, + readresponse: 0, + lampfailure: 0, + ballastfailure: 1, + convertorerror: 0, + }, + }, + { + apdu_data: [0x02, 0x08], + jsval: { + address: 8, + addresstype: 0, + readresponse: 0, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 1, + }, + }, +]) run('DPT237.600', [ - { apdu_data: [0x00, 0x01], jsval: {address: 1, addresstype: 0, - readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, - { apdu_data: [0x00, 0x05], jsval: {address: 5, addresstype: 0, - readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, - { apdu_data: [0x00, 0x06], jsval: {address: 6, addresstype: 0, - readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, - { apdu_data: [0x00, 0x08], jsval: {address: 8, addresstype: 0, - readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}} -]); + { + apdu_data: [0x00, 0x01], + jsval: { + address: 1, + addresstype: 0, + readresponse: 0, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 0, + }, + }, + { + apdu_data: [0x00, 0x05], + jsval: { + address: 5, + addresstype: 0, + readresponse: 0, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 0, + }, + }, + { + apdu_data: [0x00, 0x06], + jsval: { + address: 6, + addresstype: 0, + readresponse: 0, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 0, + }, + }, + { + apdu_data: [0x00, 0x08], + jsval: { + address: 8, + addresstype: 0, + readresponse: 0, + lampfailure: 0, + ballastfailure: 0, + convertorerror: 0, + }, + }, +]) diff --git a/test/dptlib/test-dpt3.ts b/test/dptlib/test-dpt3.ts index 277008e..1df5dc0 100644 --- a/test/dptlib/test-dpt3.ts +++ b/test/dptlib/test-dpt3.ts @@ -1,18 +1,18 @@ -import { run } from "./commontest"; +import { run } from './commontest' /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ run('DPT3', [ - { apdu_data: [0x00], jsval: {decr_incr: 0, data: 0}}, - { apdu_data: [0x06], jsval: {decr_incr: 0, data: 6}} -]); + { apdu_data: [0x00], jsval: { decr_incr: 0, data: 0 } }, + { apdu_data: [0x06], jsval: { decr_incr: 0, data: 6 } }, +]) run('DPT3.007', [ - { apdu_data: [0x01], jsval: {decr_incr: 0, data: 1}}, - { apdu_data: [0x05], jsval: {decr_incr: 0, data: 5}}, - { apdu_data: [0x08], jsval: {decr_incr: 1, data: 0}}, - { apdu_data: [0x0f], jsval: {decr_incr: 1, data: 7}} -]); + { apdu_data: [0x01], jsval: { decr_incr: 0, data: 1 } }, + { apdu_data: [0x05], jsval: { decr_incr: 0, data: 5 } }, + { apdu_data: [0x08], jsval: { decr_incr: 1, data: 0 } }, + { apdu_data: [0x0f], jsval: { decr_incr: 1, data: 7 } }, +]) diff --git a/test/dptlib/test-dpt4.ts b/test/dptlib/test-dpt4.ts index fe0cd14..030592c 100644 --- a/test/dptlib/test-dpt4.ts +++ b/test/dptlib/test-dpt4.ts @@ -1,11 +1,11 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT4', [ - { apdu_data: [0x40], jsval: "@"}, - { apdu_data: [0x76], jsval: "v"} -]); + { apdu_data: [0x40], jsval: '@' }, + { apdu_data: [0x76], jsval: 'v' }, +]) diff --git a/test/dptlib/test-dpt5.ts b/test/dptlib/test-dpt5.ts index 3c870a9..29b9b7d 100644 --- a/test/dptlib/test-dpt5.ts +++ b/test/dptlib/test-dpt5.ts @@ -1,27 +1,27 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' // DPT5 without subtype: no scaling run('DPT5', [ - { apdu_data: [0x00], jsval: 0}, - { apdu_data: [0x40], jsval: 64}, - { apdu_data: [0x41], jsval: 65}, - { apdu_data: [0x80], jsval: 128}, - { apdu_data: [0xff], jsval: 255} -]); + { apdu_data: [0x00], jsval: 0 }, + { apdu_data: [0x40], jsval: 64 }, + { apdu_data: [0x41], jsval: 65 }, + { apdu_data: [0x80], jsval: 128 }, + { apdu_data: [0xff], jsval: 255 }, +]) // 5.001 percentage (0=0..ff=100%) run('DPT5.001', [ - { apdu_data: [0x00], jsval: 0 }, - { apdu_data: [0x80], jsval: 50}, - { apdu_data: [0xff], jsval: 100} -]); + { apdu_data: [0x00], jsval: 0 }, + { apdu_data: [0x80], jsval: 50 }, + { apdu_data: [0xff], jsval: 100 }, +]) // 5.003 angle (degrees 0=0, ff=360) run('DPT5.003', [ - { apdu_data: [0x00], jsval: 0 }, - { apdu_data: [0x80], jsval: 181 }, - { apdu_data: [0xff], jsval: 360 } -]); + { apdu_data: [0x00], jsval: 0 }, + { apdu_data: [0x80], jsval: 181 }, + { apdu_data: [0xff], jsval: 360 }, +]) diff --git a/test/dptlib/test-dpt6.ts b/test/dptlib/test-dpt6.ts index 6d18cb1..3ce424f 100644 --- a/test/dptlib/test-dpt6.ts +++ b/test/dptlib/test-dpt6.ts @@ -1,13 +1,13 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT6', [ - { apdu_data: [0x00], jsval: 0}, - { apdu_data: [0x7f], jsval: 127}, - { apdu_data: [0x80], jsval: -128}, - { apdu_data: [0xff], jsval: -1} -]); + { apdu_data: [0x00], jsval: 0 }, + { apdu_data: [0x7f], jsval: 127 }, + { apdu_data: [0x80], jsval: -128 }, + { apdu_data: [0xff], jsval: -1 }, +]) diff --git a/test/dptlib/test-dpt7.ts b/test/dptlib/test-dpt7.ts index 026c5fd..ec57773 100644 --- a/test/dptlib/test-dpt7.ts +++ b/test/dptlib/test-dpt7.ts @@ -1,13 +1,13 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT7', [ - { apdu_data: [0x00, 0x11], jsval: 17}, - { apdu_data: [0x01, 0x00], jsval: 256}, - { apdu_data: [0x10, 0x01], jsval: 4097}, - { apdu_data: [0xff, 0xff], jsval: 65535}, -]); + { apdu_data: [0x00, 0x11], jsval: 17 }, + { apdu_data: [0x01, 0x00], jsval: 256 }, + { apdu_data: [0x10, 0x01], jsval: 4097 }, + { apdu_data: [0xff, 0xff], jsval: 65535 }, +]) diff --git a/test/dptlib/test-dpt8.ts b/test/dptlib/test-dpt8.ts index c2c60d3..1a21fa4 100644 --- a/test/dptlib/test-dpt8.ts +++ b/test/dptlib/test-dpt8.ts @@ -1,14 +1,14 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT8', [ - { apdu_data: [0x00, 0x11], jsval: 17}, - { apdu_data: [0x01, 0x00], jsval: 256}, - { apdu_data: [0x7f, 0xff], jsval: 32767}, - { apdu_data: [0x80, 0x00], jsval: -32768}, - { apdu_data: [0xff, 0xff], jsval: -1} -]); + { apdu_data: [0x00, 0x11], jsval: 17 }, + { apdu_data: [0x01, 0x00], jsval: 256 }, + { apdu_data: [0x7f, 0xff], jsval: 32767 }, + { apdu_data: [0x80, 0x00], jsval: -32768 }, + { apdu_data: [0xff, 0xff], jsval: -1 }, +]) diff --git a/test/dptlib/test-dpt9.ts b/test/dptlib/test-dpt9.ts index b0f8453..d1e569d 100644 --- a/test/dptlib/test-dpt9.ts +++ b/test/dptlib/test-dpt9.ts @@ -1,18 +1,18 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import { run } from "./commontest"; +import { run } from './commontest' run('DPT9', [ - { apdu_data: [0x00, 0x02], jsval: 0.02}, - { apdu_data: [0x87, 0xfe], jsval: -0.02}, - { apdu_data: [0x02, 0xf8], jsval: 7.6}, - { apdu_data: [0x0c, 0x24], jsval: 21.2}, - { apdu_data: [0x0c, 0x7e], jsval: 23}, - { apdu_data: [0x5c, 0xc4], jsval: 24985.6}, - { apdu_data: [0xdb, 0x3c], jsval: -24985.6}, - { apdu_data: [0x7f, 0xfe], jsval: 670433.28}, - { apdu_data: [0xf8, 0x02], jsval: -670433.28}, -]); + { apdu_data: [0x00, 0x02], jsval: 0.02 }, + { apdu_data: [0x87, 0xfe], jsval: -0.02 }, + { apdu_data: [0x02, 0xf8], jsval: 7.6 }, + { apdu_data: [0x0c, 0x24], jsval: 21.2 }, + { apdu_data: [0x0c, 0x7e], jsval: 23 }, + { apdu_data: [0x5c, 0xc4], jsval: 24985.6 }, + { apdu_data: [0xdb, 0x3c], jsval: -24985.6 }, + { apdu_data: [0x7f, 0xfe], jsval: 670433.28 }, + { apdu_data: [0xf8, 0x02], jsval: -670433.28 }, +]) diff --git a/test/knxproto/test-address.ts b/test/knxproto/test-address.ts index 44165f9..df28e3e 100644 --- a/test/knxproto/test-address.ts +++ b/test/knxproto/test-address.ts @@ -1,66 +1,80 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import * as address from '../../src/Address'; -import test from 'tape'; +import * as address from '../../src/Address' +import test from 'tape' // -test('KNX physical address test', function(t) { - var tests = { - "0.0.0": Buffer.from([0, 0]), - "0.0.10": Buffer.from([0, 10]), - "0.0.255": Buffer.from([0, 255]), - "0.1.0": Buffer.from([1, 0]), - "1.0.0": Buffer.from([16, 0]), - "15.14.0": Buffer.from([254, 0]), - "15.15.0": Buffer.from([255, 0]), - }; - Object.keys(tests).forEach((key, idx) => { - var buf = tests[key]; - var encoded = address.parse(key, address.TYPE.PHYSICAL); - t.ok(Buffer.compare(encoded, buf) == 0, `Marshaled KNX physical address ${key}: encoded=${encoded.toString()} buf=${buf.toString()}`); - var decoded = address.toString(encoded, address.TYPE.PHYSICAL); - t.ok(decoded == key, `${key}: unmarshaled KNX physical address`); - }); - // test invalid physical addresses - var invalid = ["0.0.", "0.0.256", "123122312312312", "16.0.0", "15.17.13"]; - for (var i in invalid) { - var key = invalid[i]; - t.throws(() => { - address.parse(key); - }, null, `invalid KNX physical address ${key}`); - } - t.end(); -}); +test('KNX physical address test', function (t) { + const tests = { + '0.0.0': Buffer.from([0, 0]), + '0.0.10': Buffer.from([0, 10]), + '0.0.255': Buffer.from([0, 255]), + '0.1.0': Buffer.from([1, 0]), + '1.0.0': Buffer.from([16, 0]), + '15.14.0': Buffer.from([254, 0]), + '15.15.0': Buffer.from([255, 0]), + } + Object.keys(tests).forEach((key, idx) => { + const buf = tests[key] + const encoded = address.parse(key, address.TYPE.PHYSICAL) + t.ok( + Buffer.compare(encoded, buf) === 0, + `Marshaled KNX physical address ${key}: encoded=${encoded.toString()} buf=${buf.toString()}`, + ) + const decoded = address.toString(encoded, address.TYPE.PHYSICAL) + t.ok(decoded === key, `${key}: unmarshaled KNX physical address`) + }) + // test invalid physical addresses + const invalid = ['0.0.', '0.0.256', '123122312312312', '16.0.0', '15.17.13'] + for (const i in invalid) { + const key = invalid[i] + t.throws( + () => { + address.parse(key) + }, + null, + `invalid KNX physical address ${key}`, + ) + } + t.end() +}) // -test('KNX group address test', function(t) { - var tests = { - "0/0/0": Buffer.from([0, 0]), - "0/0/10": Buffer.from([0, 10]), - "0/0/255": Buffer.from([0, 255]), - "0/1/0": Buffer.from([1, 0]), - "1/0/0": Buffer.from([8, 0]), - "1/7/0": Buffer.from([15, 0]), - "31/6/0": Buffer.from([254, 0]), - "31/7/0": Buffer.from([255, 0]), - }; - Object.keys(tests).forEach((key, idx) => { - var buf = tests[key]; - var encoded = address.parse(key, address.TYPE.GROUP); - t.ok(Buffer.compare(encoded, buf) == 0, `Marshaled KNX group address ${key}: encoded=${encoded.toString('hex')} buf=${buf.toString('hex')}`); - var decoded = address.toString(encoded, address.TYPE.GROUP); - t.ok(decoded == key, `${key}: unmarshaled KNX group address`); - }); +test('KNX group address test', function (t) { + const tests = { + '0/0/0': Buffer.from([0, 0]), + '0/0/10': Buffer.from([0, 10]), + '0/0/255': Buffer.from([0, 255]), + '0/1/0': Buffer.from([1, 0]), + '1/0/0': Buffer.from([8, 0]), + '1/7/0': Buffer.from([15, 0]), + '31/6/0': Buffer.from([254, 0]), + '31/7/0': Buffer.from([255, 0]), + } + Object.keys(tests).forEach((key, idx) => { + const buf = tests[key] + const encoded = address.parse(key, address.TYPE.GROUP) + t.ok( + Buffer.compare(encoded, buf) === 0, + `Marshaled KNX group address ${key}: encoded=${encoded.toString('hex')} buf=${buf.toString('hex')}`, + ) + const decoded = address.toString(encoded, address.TYPE.GROUP) + t.ok(decoded === key, `${key}: unmarshaled KNX group address`) + }) - var invalid = ["0/0/", "0/0/256", "123122312312312", "16/0/0", "15/17/13"]; - for (var i in invalid) { - var key = invalid[i]; - t.throws(() => { - address.parse(key); - }, null, `invalid KNX group address ${key}`); - } - t.end(); -}); + const invalid = ['0/0/', '0/0/256', '123122312312312', '16/0/0', '15/17/13'] + for (const i in invalid) { + const key = invalid[i] + t.throws( + () => { + address.parse(key) + }, + null, + `invalid KNX group address ${key}`, + ) + } + t.end() +}) diff --git a/test/knxproto/test-proto.ts b/test/knxproto/test-proto.ts index 500d83a..1784214 100644 --- a/test/knxproto/test-proto.ts +++ b/test/knxproto/test-proto.ts @@ -1,259 +1,283 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ -import KnxNetProtocol from '../../src/KnxProtocol'; -import test from 'tape'; +import KnxNetProtocol from '../../src/KnxProtocol' +import test from 'tape' -KnxNetProtocol.debug = true; +KnxNetProtocol.debug = true // -test('KNX protocol unmarshaller', function(t) { - var tests = { - "ETS5 programming request": Buffer.from([ - 6, 16, 4, 32, 0, 20, 4, 34, - 1, 0, 197, 0, 17, 252, 17, 253, - 17, 1, 0, 238 - ]) - }; - Object.keys(tests).forEach((key, idx) => { - var buf = tests[key]; - // unmarshal from a buffer... - var reader = KnxNetProtocol.createReader(buf); - var writer = KnxNetProtocol.createWriter(); - reader.KNXNetHeader('tmp'); - var decoded = reader.next()['tmp']; - console.log("\n=== %s: %j ===> %j", key, buf, decoded); - t.ok(decoded != undefined, `${key}: unmarshaled KNX datagram`); - }); - t.end(); -}); +test('KNX protocol unmarshaller', function (t) { + const tests = { + 'ETS5 programming request': Buffer.from([ + 6, 16, 4, 32, 0, 20, 4, 34, 1, 0, 197, 0, 17, 252, 17, 253, 17, 1, + 0, 238, + ]), + } + Object.keys(tests).forEach((key, idx) => { + const buf = tests[key] + // unmarshal from a buffer... + const reader = KnxNetProtocol.createReader(buf) + const writer = KnxNetProtocol.createWriter() + reader.KNXNetHeader('tmp') + const decoded = reader.next()['tmp'] + console.log('\n=== %s: %j ===> %j', key, buf, decoded) + t.ok(decoded !== undefined, `${key}: unmarshaled KNX datagram`) + }) + t.end() +}) -test('KNX protocol marshal+unmarshal', function(t) { - var tests = { - CONNECT_REQUEST: Buffer.from( - "06100205001a0801c0a80ab3d96d0801c0a80ab3d83604040200", 'hex'), - CONNECT_RESPONSE: Buffer.from( - "061002060014030008010a0c17350e5704040000", 'hex'), - "CONNECT_RESPONSE, failure E_NO_MORE_CONNECTIONS: 0x24": Buffer.from( - "0610020600080024", 'hex'), - "tunneling request (GroupValue_Read) apdu=1byte": Buffer.from( - "061004200015040200002e00bce000000832010000", 'hex'), - "tunneling request (GroupValue_Write) apdu=1byte": Buffer.from( - "061004200015040200002e00bce000000832010081", 'hex'), - "tunneling request (GroupValue_Write) apdu=2byte": Buffer.from( - "061004200016040201002900bce00000083b0200804a", 'hex'), - "routing indication": Buffer.from( - "0610053000112900bce0ff0f0908010000", 'hex'), - DISCONNECT_REQUEST: Buffer.from([ - 6, 16, 2, 9, 0, 16, 142, 142, - 8, 1, 192, 168, 2, 222, 14, 87 - ]), - }; - Object.keys(tests).forEach((key, idx) => { - var buf = tests[key]; - // unmarshal from a buffer... - var reader = KnxNetProtocol.createReader(buf); - var writer = KnxNetProtocol.createWriter(); - reader.KNXNetHeader('tmp'); - var decoded = reader.next()['tmp']; - console.log("\n=== %s: %j ===> %j", key, buf, decoded); - t.ok(decoded != undefined, `${key}: unmarshaled KNX datagram`); - // then marshal the datagram again into a buffer... - writer.KNXNetHeader(decoded); - if (Buffer.compare(buf, writer.buffer) != 0) { - console.log("\n\n========\n FAIL: %s\n========\nbuffer is different:\n", key); - console.log(' 0 1 2 3 4 5|6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9') - console.log('expected : %s', buf.toString('hex')); - console.log('got instead: %s', writer.buffer.toString('hex')); - } - t.ok(Buffer.compare(buf, writer.buffer) == 0); - }); - t.end(); -}); +test('KNX protocol marshal+unmarshal', function (t) { + const tests = { + CONNECT_REQUEST: Buffer.from( + '06100205001a0801c0a80ab3d96d0801c0a80ab3d83604040200', + 'hex', + ), + CONNECT_RESPONSE: Buffer.from( + '061002060014030008010a0c17350e5704040000', + 'hex', + ), + 'CONNECT_RESPONSE, failure E_NO_MORE_CONNECTIONS: 0x24': Buffer.from( + '0610020600080024', + 'hex', + ), + 'tunneling request (GroupValue_Read) apdu=1byte': Buffer.from( + '061004200015040200002e00bce000000832010000', + 'hex', + ), + 'tunneling request (GroupValue_Write) apdu=1byte': Buffer.from( + '061004200015040200002e00bce000000832010081', + 'hex', + ), + 'tunneling request (GroupValue_Write) apdu=2byte': Buffer.from( + '061004200016040201002900bce00000083b0200804a', + 'hex', + ), + 'routing indication': Buffer.from( + '0610053000112900bce0ff0f0908010000', + 'hex', + ), + DISCONNECT_REQUEST: Buffer.from([ + 6, 16, 2, 9, 0, 16, 142, 142, 8, 1, 192, 168, 2, 222, 14, 87, + ]), + } + Object.keys(tests).forEach((key, idx) => { + const buf = tests[key] + // unmarshal from a buffer... + const reader = KnxNetProtocol.createReader(buf) + const writer = KnxNetProtocol.createWriter() + reader.KNXNetHeader('tmp') + const decoded = reader.next()['tmp'] + console.log('\n=== %s: %j ===> %j', key, buf, decoded) + t.ok(decoded !== undefined, `${key}: unmarshaled KNX datagram`) + // then marshal the datagram again into a buffer... + writer.KNXNetHeader(decoded) + if (Buffer.compare(buf, writer.buffer) !== 0) { + console.log( + '\n\n========\n FAIL: %s\n========\nbuffer is different:\n', + key, + ) + console.log( + ' 0 1 2 3 4 5|6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9', + ) + console.log('expected : %s', buf.toString('hex')) + console.log('got instead: %s', writer.buffer.toString('hex')) + } + t.ok(Buffer.compare(buf, writer.buffer) === 0) + }) + t.end() +}) -test('KNX protocol marshaller', function(t) { - var tests = { - "compose tunneling request (write) apdu=1byte - turn ON a light": { - hexbuf: "061004200015040200002e00bce000000832010081", - dgram: { - header_length: 6, - protocol_version: 16, - service_type: 1056, - total_length: 21, - tunnstate: { - header_length: 4, - channel_id: 2, - seqnum: 0, - rsvd: 0 - }, - cemi: { - msgcode: 46, - addinfo_length: 0, - ctrl: { - frameType: 1, - reserved: 0, - repeat: 1, - broadcast: 1, - priority: 3, - acknowledge: 0, - confirm: 0, - destAddrType: 1, - hopCount: 6, - extendedFrame: 0 - }, - src_addr: '0.0.0', - dest_addr: '1/0/50', - apdu: { - tpci: 0, - apci: 'GroupValue_Write', - data: 1 - } - } - } - }, +test('KNX protocol marshaller', function (t) { + const tests = { + 'compose tunneling request (write) apdu=1byte - turn ON a light': { + hexbuf: '061004200015040200002e00bce000000832010081', + dgram: { + header_length: 6, + protocol_version: 16, + service_type: 1056, + total_length: 21, + tunnstate: { + header_length: 4, + channel_id: 2, + seqnum: 0, + rsvd: 0, + }, + cemi: { + msgcode: 46, + addinfo_length: 0, + ctrl: { + frameType: 1, + reserved: 0, + repeat: 1, + broadcast: 1, + priority: 3, + acknowledge: 0, + confirm: 0, + destAddrType: 1, + hopCount: 6, + extendedFrame: 0, + }, + src_addr: '0.0.0', + dest_addr: '1/0/50', + apdu: { + tpci: 0, + apci: 'GroupValue_Write', + data: 1, + }, + }, + }, + }, - "compose tunneling request (write) apdu=1byte - turn OFF a light": { - hexbuf: "061004200015040200002e00bce000000832010080", - dgram: { - header_length: 6, - protocol_version: 16, - service_type: 1056, - total_length: 21, - tunnstate: { - header_length: 4, - channel_id: 2, - seqnum: 0, - rsvd: 0 - }, - cemi: { - msgcode: 46, - addinfo_length: 0, - ctrl: { - frameType: 1, - reserved: 0, - repeat: 1, - broadcast: 1, - priority: 3, - acknowledge: 0, - confirm: 0, - destAddrType: 1, - hopCount: 6, - extendedFrame: 0 - }, - src_addr: '0.0.0', - dest_addr: '1/0/50', - apdu: { - tpci: 0, - apci: 'GroupValue_Write', - data: [0] - } - } - } - }, + 'compose tunneling request (write) apdu=1byte - turn OFF a light': { + hexbuf: '061004200015040200002e00bce000000832010080', + dgram: { + header_length: 6, + protocol_version: 16, + service_type: 1056, + total_length: 21, + tunnstate: { + header_length: 4, + channel_id: 2, + seqnum: 0, + rsvd: 0, + }, + cemi: { + msgcode: 46, + addinfo_length: 0, + ctrl: { + frameType: 1, + reserved: 0, + repeat: 1, + broadcast: 1, + priority: 3, + acknowledge: 0, + confirm: 0, + destAddrType: 1, + hopCount: 6, + extendedFrame: 0, + }, + src_addr: '0.0.0', + dest_addr: '1/0/50', + apdu: { + tpci: 0, + apci: 'GroupValue_Write', + data: [0], + }, + }, + }, + }, - "compose tunneling request (write) apdu=2byte - DIMMING a light to 10%": { - hexbuf: "061004200016040200002e00bce0000008320200800a", - dgram: { - header_length: 6, - protocol_version: 16, - service_type: 1056, - total_length: 21, - tunnstate: { - header_length: 4, - channel_id: 2, - seqnum: 0, - rsvd: 0 - }, - cemi: { - msgcode: 46, - addinfo_length: 0, - ctrl: { - frameType: 1, - reserved: 0, - repeat: 1, - broadcast: 1, - priority: 3, - acknowledge: 0, - confirm: 0, - destAddrType: 1, - hopCount: 6, - extendedFrame: 0 - }, - src_addr: '0.0.0', - dest_addr: '1/0/50', - apdu: { - bitlength: 8, - tpci: 0, - apci: 'GroupValue_Write', - data: [10] - } - } - } - }, + 'compose tunneling request (write) apdu=2byte - DIMMING a light to 10%': + { + hexbuf: '061004200016040200002e00bce0000008320200800a', + dgram: { + header_length: 6, + protocol_version: 16, + service_type: 1056, + total_length: 21, + tunnstate: { + header_length: 4, + channel_id: 2, + seqnum: 0, + rsvd: 0, + }, + cemi: { + msgcode: 46, + addinfo_length: 0, + ctrl: { + frameType: 1, + reserved: 0, + repeat: 1, + broadcast: 1, + priority: 3, + acknowledge: 0, + confirm: 0, + destAddrType: 1, + hopCount: 6, + extendedFrame: 0, + }, + src_addr: '0.0.0', + dest_addr: '1/0/50', + apdu: { + bitlength: 8, + tpci: 0, + apci: 'GroupValue_Write', + data: [10], + }, + }, + }, + }, - "temperature response, apdu=2-byte": { - hexbuf: "061004200017040200002e00BCD0110B000F0300400730", - dgram: { - header_length: 6, - protocol_version: 16, - service_type: 1056, - total_length: 22, - tunnstate: { - header_length: 4, - channel_id: 2, - seqnum: 0, - rsvd: 0 - }, - cemi: { - msgcode: 46, - addinfo_length: 0, - ctrl: { - frameType: 1, - reserved: 0, - repeat: 1, - broadcast: 1, - priority: 3, - acknowledge: 0, - confirm: 0, - destAddrType: 1, - hopCount: 5, - extendedFrame: 0 - }, - src_addr: '1.1.11', - dest_addr: '0/0/15', - apdu: { - bitlength: 16, - tpci: 0, - apci: 'GroupValue_Response', - data: [0x07, 0x30] - } - } - } - }, - } + 'temperature response, apdu=2-byte': { + hexbuf: '061004200017040200002e00BCD0110B000F0300400730', + dgram: { + header_length: 6, + protocol_version: 16, + service_type: 1056, + total_length: 22, + tunnstate: { + header_length: 4, + channel_id: 2, + seqnum: 0, + rsvd: 0, + }, + cemi: { + msgcode: 46, + addinfo_length: 0, + ctrl: { + frameType: 1, + reserved: 0, + repeat: 1, + broadcast: 1, + priority: 3, + acknowledge: 0, + confirm: 0, + destAddrType: 1, + hopCount: 5, + extendedFrame: 0, + }, + src_addr: '1.1.11', + dest_addr: '0/0/15', + apdu: { + bitlength: 16, + tpci: 0, + apci: 'GroupValue_Response', + data: [0x07, 0x30], + }, + }, + }, + }, + } - - Object.keys(tests).forEach((key, idx) => { - var testcase = tests[key]; - var buf = typeof testcase.hexbuf == 'string' ? - Buffer.from(testcase.hexbuf.replace(/\s/g, ''), 'hex') : testcase.hexbuf; - console.log("\n=== %s", key); - // marshal the test datagram - var writer = KnxNetProtocol.createWriter(); - writer.KNXNetHeader(testcase.dgram); - if (Buffer.compare(buf, writer.buffer) != 0) { - // if this fails, unmarshal the buffer again to a datagram - var reader = KnxNetProtocol.createReader(writer.buffer); - reader.KNXNetHeader('tmp'); - var decoded = reader.next()['tmp']; - console.log("\n\n========\n FAIL: %s\n========\nbuffer is different:\n", key); - console.log(' 0 1 2 3 4 5|6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9') - console.log('expected : %s', buf.toString('hex')); - console.log('got instead: %s', writer.buffer.toString('hex')); - } - t.ok(Buffer.compare(buf, writer.buffer) == 0); - }); - t.end(); -}); + Object.keys(tests).forEach((key, idx) => { + const testcase = tests[key] + const buf = + typeof testcase.hexbuf === 'string' + ? Buffer.from(testcase.hexbuf.replace(/\s/g, ''), 'hex') + : testcase.hexbuf + console.log('\n=== %s', key) + // marshal the test datagram + const writer = KnxNetProtocol.createWriter() + writer.KNXNetHeader(testcase.dgram) + if (Buffer.compare(buf, writer.buffer) !== 0) { + // if this fails, unmarshal the buffer again to a datagram + const reader = KnxNetProtocol.createReader(writer.buffer) + reader.KNXNetHeader('tmp') + const decoded = reader.next()['tmp'] + console.log( + '\n\n========\n FAIL: %s\n========\nbuffer is different:\n', + key, + ) + console.log( + ' 0 1 2 3 4 5|6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9', + ) + console.log('expected : %s', buf.toString('hex')) + console.log('got instead: %s', writer.buffer.toString('hex')) + } + t.ok(Buffer.compare(buf, writer.buffer) === 0) + }) + t.end() +}) diff --git a/test/wiredtests/test-connect-routing-hybrid.ts b/test/wiredtests/test-connect-routing-hybrid.ts index 049bfed..d44e2fd 100644 --- a/test/wiredtests/test-connect-routing-hybrid.ts +++ b/test/wiredtests/test-connect-routing-hybrid.ts @@ -2,10 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -Error.stackTraceLimit = Infinity; +import { Connection } from '../../src' +import test from 'tape' -import { Connection } from "../../src"; -import test from "tape"; +Error.stackTraceLimit = Infinity /* ========== ================== @@ -13,26 +13,26 @@ import test from "tape"; ========== ================== */ // -test("KNX connect routing hybrid", function (t) { - var connection = new Connection({ - loglevel: "debug", - forceTunneling: true, - handlers: { - connected: function () { - t.pass("connected in hybrid mode"); - t.end(); - process.exit(0); - }, - error: function (connstatus) { - t.fail("error connecting: " + connstatus); - t.end(); - process.exit(1); - }, - }, - }); -}); +test('KNX connect routing hybrid', function (t) { + const connection = new Connection({ + loglevel: 'debug', + forceTunneling: true, + handlers: { + connected() { + t.pass('connected in hybrid mode') + t.end() + process.exit(0) + }, + error(connstatus) { + t.fail(`error connecting: ${connstatus}`) + t.end() + process.exit(1) + }, + }, + }) +}) setTimeout(function () { - console.log("Exiting with timeout..."); - process.exit(2); -}, 1000); + console.log('Exiting with timeout...') + process.exit(2) +}, 1000) diff --git a/test/wiredtests/test-connect-tunnel.ts b/test/wiredtests/test-connect-tunnel.ts index c197e1d..71ed3bc 100644 --- a/test/wiredtests/test-connect-tunnel.ts +++ b/test/wiredtests/test-connect-tunnel.ts @@ -3,12 +3,12 @@ * (C) 2016-2018 Elias Karakoulakis */ -Error.stackTraceLimit = Infinity; +import { Connection } from '../../src' +import test from 'tape' -import { Connection } from "../../src"; -import test from "tape"; +import options from './wiredtest-options' -import options from "./wiredtest-options"; +Error.stackTraceLimit = Infinity /* ========== ================== @@ -16,36 +16,36 @@ import options from "./wiredtest-options"; ========== ================== */ // -test("KNX connect tunneling", function (t) { - var connection = new Connection({ - // set up your KNX IP router's IP address (not multicast!) - // for getting into tunnelling mode - ipAddr: options.ipAddr, - physAddr: options.physAddr, - debug: true, - handlers: { - connected: function () { - console.log("----------"); - console.log("Connected!"); - console.log("----------"); - t.pass("connected in TUNNELING mode"); - this.Disconnect(); - }, - disconnected: function () { - t.pass("disconnected in TUNNELING mode"); - t.end(); - process.exit(0); - }, - error: function (connstatus) { - t.fail("error connecting: " + connstatus); - t.end(); - process.exit(1); - }, - }, - }); -}); +test('KNX connect tunneling', function (t) { + const connection = new Connection({ + // set up your KNX IP router's IP address (not multicast!) + // for getting into tunnelling mode + ipAddr: options.ipAddr, + physAddr: options.physAddr, + debug: true, + handlers: { + connected() { + console.log('----------') + console.log('Connected!') + console.log('----------') + t.pass('connected in TUNNELING mode') + this.Disconnect() + }, + disconnected() { + t.pass('disconnected in TUNNELING mode') + t.end() + process.exit(0) + }, + error(connstatus) { + t.fail(`error connecting: ${connstatus}`) + t.end() + process.exit(1) + }, + }, + }) +}) setTimeout(function () { - console.log("Exiting with timeout..."); - process.exit(2); -}, 1000); + console.log('Exiting with timeout...') + process.exit(2) +}, 1000) diff --git a/test/wiredtests/test-control.ts b/test/wiredtests/test-control.ts index a383982..57865eb 100644 --- a/test/wiredtests/test-control.ts +++ b/test/wiredtests/test-control.ts @@ -1,76 +1,99 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ -Error.stackTraceLimit = Infinity; + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ +import { Connection, Datapoint } from '../../src' +import test from 'tape' -import { Connection, Datapoint } from '../../src'; -import test from "tape"; +import options from './wiredtest-options' -import options from "./wiredtest-options"; +Error.stackTraceLimit = Infinity /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== */ - test('KNX wired test - control a basic DPT1 binary switch', function(t) { - var counter = 0; - var connection = new Connection({ - debug: true, - physAddr: options.physAddr, - handlers: { - connected: function() { - console.log('----------'); - console.log('Connected!'); - console.log('----------'); - var light = new Datapoint({ - ga: options.wired_test_control_ga, - dpt: 'DPT1.001' - }, connection); - light.on('change', () => { - counter += 1; - if (counter == 4) { - t.pass('all 4 responses received'); - t.end(); - process.exit(0); - } - }); - // operation 1 - light.write(0); - // operation 2 - setTimeout(function() { - light.write(1); - }, 500); - // issue #71 - writing to an invalid address should not stall the FSM - connection.write('10/10/5', 1); - // operation 3 - Do the same with writeRaw - setTimeout(function() { - connection.writeRaw(options.wired_test_control_ga, Buffer.from('00', 'hex'), 1); - }, 1000); - // operation 4 - Do the same with writeRaw - setTimeout(function() { - connection.writeRaw(options.wired_test_control_ga, Buffer.from('01', 'hex'), 0); - }, 1500); - }, - event: function(evt, src, dest, value) { - console.log("%s ===> %s <===, src: %j, dest: %j, value: %j", - new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), - evt, src, dest, value - ); - }, - error: function(connstatus) { - console.log("%s **** ERROR: %j", - new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), - connstatus); - t.fail('error: '+connstatus); - process.exit(1); - } - } - }); - }) +test('KNX wired test - control a basic DPT1 binary switch', function (t) { + let counter = 0 + const connection = new Connection({ + debug: true, + physAddr: options.physAddr, + handlers: { + connected() { + console.log('----------') + console.log('Connected!') + console.log('----------') + const light = new Datapoint( + { + ga: options.wired_test_control_ga, + dpt: 'DPT1.001', + }, + connection, + ) + light.on('change', () => { + counter += 1 + if (counter === 4) { + t.pass('all 4 responses received') + t.end() + process.exit(0) + } + }) + // operation 1 + light.write(0) + // operation 2 + setTimeout(function () { + light.write(1) + }, 500) + // issue #71 - writing to an invalid address should not stall the FSM + connection.write('10/10/5', 1) + // operation 3 - Do the same with writeRaw + setTimeout(function () { + connection.writeRaw( + options.wired_test_control_ga, + Buffer.from('00', 'hex'), + 1, + ) + }, 1000) + // operation 4 - Do the same with writeRaw + setTimeout(function () { + connection.writeRaw( + options.wired_test_control_ga, + Buffer.from('01', 'hex'), + 0, + ) + }, 1500) + }, + event(evt, src, dest, value) { + console.log( + '%s ===> %s <===, src: %j, dest: %j, value: %j', + new Date() + .toISOString() + .replace(/T/, ' ') + .replace(/Z$/, ''), + evt, + src, + dest, + value, + ) + }, + error(connstatus) { + console.log( + '%s **** ERROR: %j', + new Date() + .toISOString() + .replace(/T/, ' ') + .replace(/Z$/, ''), + connstatus, + ) + t.fail(`error: ${connstatus}`) + process.exit(1) + }, + }, + }) +}) - setTimeout(function() { - console.log('Exiting with timeout ...'); - process.exit(2); - }, 2000); +setTimeout(function () { + console.log('Exiting with timeout ...') + process.exit(2) +}, 2000) diff --git a/test/wiredtests/test-logotimer.ts b/test/wiredtests/test-logotimer.ts index c2e2571..727f7b8 100644 --- a/test/wiredtests/test-logotimer.ts +++ b/test/wiredtests/test-logotimer.ts @@ -2,53 +2,59 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint } from "../../src"; -import test from "tape"; -import util from "util"; -import options from "./wiredtest-options"; +import { Connection, Datapoint } from '../../src' +import test from 'tape' +import util from 'util' +import options from './wiredtest-options' /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== */ -test("KNX wired test - control a DPT9 timer", function (t) { - var connection = new Connection({ - //debug: true, - handlers: { - connected: () => { - var timer_control = new Datapoint( - { - ga: options.dpt9_timer_control_ga, - dpt: "DPT9.001", - autoread: true, - }, - connection - ); - var timer_status = new Datapoint( - { ga: options.dpt9_timer_status_ga, dpt: "DPT9.001", autoread: true }, - connection - ); - timer_control.on("change", function (oldvalue, newvalue) { - t.pass( - util.format( - "**** Timer control changed from: %j to: %j", - oldvalue, - newvalue - ) - ); - }); - timer_status.read(function (src, response) { - t.pass(util.format("**** Timer status response: %j", response)); - t.end(); - process.exit(0); - }); - timer_control.write(12); - }, - }, - }); -}); +test('KNX wired test - control a DPT9 timer', function (t) { + const connection = new Connection({ + // debug: true, + handlers: { + connected: () => { + const timer_control = new Datapoint( + { + ga: options.dpt9_timer_control_ga, + dpt: 'DPT9.001', + autoread: true, + }, + connection, + ) + const timer_status = new Datapoint( + { + ga: options.dpt9_timer_status_ga, + dpt: 'DPT9.001', + autoread: true, + }, + connection, + ) + timer_control.on('change', function (oldvalue, newvalue) { + t.pass( + util.format( + '**** Timer control changed from: %j to: %j', + oldvalue, + newvalue, + ), + ) + }) + timer_status.read(function (src, response) { + t.pass( + util.format('**** Timer status response: %j', response), + ) + t.end() + process.exit(0) + }) + timer_control.write(12) + }, + }, + }) +}) setTimeout(function () { - process.exit(1); -}, 1000); + process.exit(1) +}, 1000) diff --git a/test/wiredtests/test-read.ts b/test/wiredtests/test-read.ts index 49999af..83685e5 100644 --- a/test/wiredtests/test-read.ts +++ b/test/wiredtests/test-read.ts @@ -3,50 +3,53 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint } from "../../src"; -import test from "tape"; -import util from "util"; -import options from "./wiredtest-options"; +import { Connection, Datapoint } from '../../src' +import test from 'tape' +import util from 'util' +import options from './wiredtest-options' /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== */ -test("KNX wired test - read a temperature", function (t) { - var connection = new Connection({ - debug: true, - physAddr: options.physAddr, - handlers: { - connected: function () { - // just define a temperature GA that should respond to a a GroupValue_Read request - var temperature_in = new Datapoint( - { - ga: options.dpt9_temperature_status_ga, - dpt: "DPT9.001", - }, - connection - ); - temperature_in.read(function (src, response) { - console.log("KNX response from %s: %j", src, response); - t.pass(util.format("read temperature: %s", response)); - t.end(); - process.exit(0); - }); - }, - error: function (connstatus) { - console.log( - "%s **** ERROR: %j", - new Date().toISOString().replace(/T/, " ").replace(/Z$/, ""), - connstatus - ); - process.exit(1); - }, - }, - }); -}); +test('KNX wired test - read a temperature', function (t) { + const connection = new Connection({ + debug: true, + physAddr: options.physAddr, + handlers: { + connected() { + // just define a temperature GA that should respond to a a GroupValue_Read request + const temperature_in = new Datapoint( + { + ga: options.dpt9_temperature_status_ga, + dpt: 'DPT9.001', + }, + connection, + ) + temperature_in.read(function (src, response) { + console.log('KNX response from %s: %j', src, response) + t.pass(util.format('read temperature: %s', response)) + t.end() + process.exit(0) + }) + }, + error(connstatus) { + console.log( + '%s **** ERROR: %j', + new Date() + .toISOString() + .replace(/T/, ' ') + .replace(/Z$/, ''), + connstatus, + ) + process.exit(1) + }, + }, + }) +}) setTimeout(function () { - console.log("Exiting ..."); - process.exit(2); -}, 1500); + console.log('Exiting ...') + process.exit(2) +}, 1500) diff --git a/test/wiredtests/test-readstorm.ts b/test/wiredtests/test-readstorm.ts index 74c190c..3078eea 100644 --- a/test/wiredtests/test-readstorm.ts +++ b/test/wiredtests/test-readstorm.ts @@ -3,89 +3,94 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint } from "../../src"; -import test from "tape"; -import util from "util"; -import options from "./wiredtest-options.js"; +import { Connection, Datapoint } from '../../src' +import test from 'tape' +import util from 'util' +import options from './wiredtest-options.js' -Error.stackTraceLimit = Infinity; +Error.stackTraceLimit = Infinity /* ========== ================== this is a WIRED test and requires a real KNX IP router on the LAN ========== ================== */ -test("KNX wired test - read multiple statuses from a consecutive GA range", function (t) { - var readback = {}; - function setupDatapoint(groupadress: string, statusga: string) { - var dp = new Datapoint( - { - ga: groupadress, - status_ga: statusga, - dpt: "DPT1.001", - autoread: true, - }, - connection - ); - dp.on("change", (oldvalue, newvalue) => { - console.log("**** %s current value: %j", groupadress, newvalue); - }); - return dp; - } - function setupDatapoints() { - var ctrl_ga_arr = options.readstorm_control_ga_start.split("/"); - var stat_ga_arr = options.readstorm_status_ga_start.split("/"); - for (let i = 0; i < options.readstorm_range; i++) { - var ctrl_ga = [ - ctrl_ga_arr[0], - ctrl_ga_arr[1], - i + parseInt(ctrl_ga_arr[2]), - ].join("/"); - var stat_ga = [ - stat_ga_arr[0], - stat_ga_arr[1], - i + parseInt(stat_ga_arr[2]), - ].join("/"); - setupDatapoint(ctrl_ga, stat_ga); - } - } - var connection = new Connection({ - loglevel: "warn", - //forceTunneling: true, - // minimumDelay: 100, - handlers: { - connected: function () { - setupDatapoints(); - }, - event: function (evt, src, dest, value) { - if (evt == "GroupValue_Response") { - readback[dest] = [src, value]; - // have we got responses from all the read requests for all datapoints? - if (Object.keys(readback).length == options.readstorm_range) { - t.pass( - util.format( - "readstorm: all %d datapoints accounted for", - options.readstorm_range - ) - ); - t.end(); - process.exit(0); - } - } - }, - error: function (connstatus) { - console.log( - "%s **** ERROR: %j", - new Date().toISOString().replace(/T/, " ").replace(/Z$/, ""), - connstatus - ); - process.exit(1); - }, - }, - }); -}); +test('KNX wired test - read multiple statuses from a consecutive GA range', function (t) { + const readback = {} + function setupDatapoint(groupadress: string, statusga: string) { + const dp = new Datapoint( + { + ga: groupadress, + status_ga: statusga, + dpt: 'DPT1.001', + autoread: true, + }, + connection, + ) + dp.on('change', (oldvalue, newvalue) => { + console.log('**** %s current value: %j', groupadress, newvalue) + }) + return dp + } + function setupDatapoints() { + const ctrl_ga_arr = options.readstorm_control_ga_start.split('/') + const stat_ga_arr = options.readstorm_status_ga_start.split('/') + for (let i = 0; i < options.readstorm_range; i++) { + const ctrl_ga = [ + ctrl_ga_arr[0], + ctrl_ga_arr[1], + i + parseInt(ctrl_ga_arr[2]), + ].join('/') + const stat_ga = [ + stat_ga_arr[0], + stat_ga_arr[1], + i + parseInt(stat_ga_arr[2]), + ].join('/') + setupDatapoint(ctrl_ga, stat_ga) + } + } + const connection = new Connection({ + loglevel: 'warn', + // forceTunneling: true, + // minimumDelay: 100, + handlers: { + connected() { + setupDatapoints() + }, + event(evt, src, dest, value) { + if (evt === 'GroupValue_Response') { + readback[dest] = [src, value] + // have we got responses from all the read requests for all datapoints? + if ( + Object.keys(readback).length === options.readstorm_range + ) { + t.pass( + util.format( + 'readstorm: all %d datapoints accounted for', + options.readstorm_range, + ), + ) + t.end() + process.exit(0) + } + } + }, + error(connstatus) { + console.log( + '%s **** ERROR: %j', + new Date() + .toISOString() + .replace(/T/, ' ') + .replace(/Z$/, ''), + connstatus, + ) + process.exit(1) + }, + }, + }) +}) setTimeout(function () { - console.log("Exiting with timeout..."); - process.exit(2); -}, 1500); + console.log('Exiting with timeout...') + process.exit(2) +}, 1500) diff --git a/test/wiredtests/wiredtest-options.ts b/test/wiredtests/wiredtest-options.ts index e3abd2d..7ab95db 100644 --- a/test/wiredtests/wiredtest-options.ts +++ b/test/wiredtests/wiredtest-options.ts @@ -1,31 +1,31 @@ /** -* knx.js - a KNX protocol stack in pure Javascript -* (C) 2016-2018 Elias Karakoulakis -*/ + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ /* define the required options for running wired tests */ - const options = { - loglevel: 'trace', - // your KNX IP router UNICAST ip address - ipAddr: '192.168.8.4', - // the physical address used by the wired tests - physAddr: '14.14.14', - // a DPT1 group address pair (binary status) to test write/read/respond - dpt1_status_ga: '1/1/1', - dpt1_control_ga: '1/1/101', - // a DPT9 group address (temperature) that should be able to respond to a GroupValue_Read request - dpt9_temperature_status_ga: '0/0/15', - // a DPT 9 control and its status GA - dpt9_timer_control_ga: '4/1/4', - dpt9_timer_status_ga: '4/1/3', - // a DPT1 group address that should also be able to respond to a GroupValue_Read request - wired_test_control_ga: '5/0/0', - // read storm test: read back statuses from an actuator with multiple relays - readstorm_control_ga_start: '1/1/0', - readstorm_status_ga_start: '1/1/100', - readstorm_range: 8 +const options = { + loglevel: 'trace', + // your KNX IP router UNICAST ip address + ipAddr: '192.168.8.4', + // the physical address used by the wired tests + physAddr: '14.14.14', + // a DPT1 group address pair (binary status) to test write/read/respond + dpt1_status_ga: '1/1/1', + dpt1_control_ga: '1/1/101', + // a DPT9 group address (temperature) that should be able to respond to a GroupValue_Read request + dpt9_temperature_status_ga: '0/0/15', + // a DPT 9 control and its status GA + dpt9_timer_control_ga: '4/1/4', + dpt9_timer_status_ga: '4/1/3', + // a DPT1 group address that should also be able to respond to a GroupValue_Read request + wired_test_control_ga: '5/0/0', + // read storm test: read back statuses from an actuator with multiple relays + readstorm_control_ga_start: '1/1/0', + readstorm_status_ga_start: '1/1/100', + readstorm_range: 8, } -export default options; +export default options From d3d8f09a0bc122993bf9129c62882560534fb081 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 15:50:55 +0200 Subject: [PATCH 09/36] fix: typo --- src/IpRoutingConnection.ts | 6 ++++-- src/IpTunnelingConnection.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index 25a70b0..3dd2bab 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -6,7 +6,9 @@ import type { KnxFSMConnection } from './FSM' function IpRoutingConnection(instance: KnxFSMConnection): KnxFSMConnection { const log = KnxLog.get() - instance.BindSocket = (cb: (socket: dgram.Socket) => void) => { + instance.BindSocket = function BindSocket( + cb: (socket: dgram.Socket) => void, + ) { const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true }) udpSocket.on('listening', () => { log.debug( @@ -37,7 +39,7 @@ function IpRoutingConnection(instance: KnxFSMConnection): KnxFSMConnection { // /// Start the connection /// - instance.Connect = () => { + instance.Connect = function Connect() { this.localAddress = this.getLocalAddress() this.socket = this.BindSocket((socket: dgram.Socket) => { socket.on('error', (errmsg: string) => diff --git a/src/IpTunnelingConnection.ts b/src/IpTunnelingConnection.ts index a36228d..d2996a7 100644 --- a/src/IpTunnelingConnection.ts +++ b/src/IpTunnelingConnection.ts @@ -5,7 +5,9 @@ import type { KnxFSMConnection } from './FSM' function IpTunnelingConnection(instance: KnxFSMConnection) { const log = KnxLog.get() - instance.BindSocket = (cb: (socket: dgram.Socket) => void) => { + instance.BindSocket = function BindSocket( + cb: (socket: dgram.Socket) => void, + ) { const udpSocket = dgram.createSocket('udp4') udpSocket.bind(() => { log.debug( @@ -18,7 +20,7 @@ function IpTunnelingConnection(instance: KnxFSMConnection) { return udpSocket } - instance.Connect = () => { + instance.Connect = function Connect() { this.localAddress = this.getLocalAddress() // create the socket this.socket = this.BindSocket((socket: dgram.Socket) => { From 196a493ee7d6ad662ee17b304b2cb4e864061140 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 15:56:24 +0200 Subject: [PATCH 10/36] fix: use default logger in dptlib --- src/dptlib/dpt1.ts | 4 ++-- src/dptlib/dpt10.ts | 4 ++-- src/dptlib/dpt11.ts | 4 ++-- src/dptlib/dpt14.ts | 4 ++-- src/dptlib/dpt16.ts | 4 ++-- src/dptlib/dpt18.ts | 4 ++-- src/dptlib/dpt19.ts | 4 ++-- src/dptlib/dpt20.ts | 4 ++-- src/dptlib/dpt21.ts | 4 ++-- src/dptlib/dpt232.ts | 4 ++-- src/dptlib/dpt237.ts | 4 ++-- src/dptlib/dpt238.ts | 4 ++-- src/dptlib/dpt9.ts | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/dptlib/dpt1.ts b/src/dptlib/dpt1.ts index 5984f35..42c516a 100644 --- a/src/dptlib/dpt1.ts +++ b/src/dptlib/dpt1.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger const custom_truthiness = (value: string | boolean): boolean => { const f = parseFloat(value as string) diff --git a/src/dptlib/dpt10.ts b/src/dptlib/dpt10.ts index 96b4a40..557717a 100644 --- a/src/dptlib/dpt10.ts +++ b/src/dptlib/dpt10.ts @@ -4,10 +4,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT10.*: time (3 bytes) diff --git a/src/dptlib/dpt11.ts b/src/dptlib/dpt11.ts index d80b68e..bf477e8 100644 --- a/src/dptlib/dpt11.ts +++ b/src/dptlib/dpt11.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT11.*: date // diff --git a/src/dptlib/dpt14.ts b/src/dptlib/dpt14.ts index db83d9d..0ab739b 100644 --- a/src/dptlib/dpt14.ts +++ b/src/dptlib/dpt14.ts @@ -2,10 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT14.*: 4-byte floating point value diff --git a/src/dptlib/dpt16.ts b/src/dptlib/dpt16.ts index 4add9c0..09e0396 100644 --- a/src/dptlib/dpt16.ts +++ b/src/dptlib/dpt16.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT16: ASCII string (max 14 chars) diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts index f54da54..b5dd0b7 100644 --- a/src/dptlib/dpt18.ts +++ b/src/dptlib/dpt18.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' import { hasProp } from 'src/utils' @@ -27,7 +27,7 @@ import { hasProp } from 'src/utils' // TODO: implement fromBuffer, formatAPDU -const log = logger.get() +const log = logger const config: DatapointConfig = { id: 'DPT18', diff --git a/src/dptlib/dpt19.ts b/src/dptlib/dpt19.ts index dc269cc..54d706e 100644 --- a/src/dptlib/dpt19.ts +++ b/src/dptlib/dpt19.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // TODO: implement fromBuffer, formatAPDU // diff --git a/src/dptlib/dpt20.ts b/src/dptlib/dpt20.ts index ed0463e..ef217d7 100644 --- a/src/dptlib/dpt20.ts +++ b/src/dptlib/dpt20.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT20: 1-byte HVAC diff --git a/src/dptlib/dpt21.ts b/src/dptlib/dpt21.ts index 2ce0c3a..a81d903 100644 --- a/src/dptlib/dpt21.ts +++ b/src/dptlib/dpt21.ts @@ -2,10 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT21: 1-byte status diff --git a/src/dptlib/dpt232.ts b/src/dptlib/dpt232.ts index 2eb02fd..8489fb1 100644 --- a/src/dptlib/dpt232.ts +++ b/src/dptlib/dpt232.ts @@ -3,10 +3,10 @@ * (C) 2016-2019 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT232: 3-byte RGB color array diff --git a/src/dptlib/dpt237.ts b/src/dptlib/dpt237.ts index 43b7bee..342e616 100644 --- a/src/dptlib/dpt237.ts +++ b/src/dptlib/dpt237.ts @@ -3,11 +3,11 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' import { hasProp } from 'src/utils' -const log = logger.get() +const log = logger // // DPT237: 2-byte unsigned value diff --git a/src/dptlib/dpt238.ts b/src/dptlib/dpt238.ts index 8c6b418..464a931 100644 --- a/src/dptlib/dpt238.ts +++ b/src/dptlib/dpt238.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT238: 1-byte unsigned value diff --git a/src/dptlib/dpt9.ts b/src/dptlib/dpt9.ts index 9273fb6..fe249ea 100644 --- a/src/dptlib/dpt9.ts +++ b/src/dptlib/dpt9.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import logger from '../KnxLog' +import { logger } from 'log-driver' import type { DatapointConfig } from '.' -const log = logger.get() +const log = logger // // DPT9.*: 2-byte floating point value From f1238d3dd2a860660fbc5841841962285ed30d0b Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 16:15:39 +0200 Subject: [PATCH 11/36] fix: typescript example --- .npmignore | 1 - run-wired-tests.sh | 2 - src/FSM.ts | 2 +- typescript-sample/package-lock.json | 453 +++++++++++++++++++++++++ typescript-sample/test-toggle-onoff.ts | 6 +- 5 files changed, 457 insertions(+), 7 deletions(-) delete mode 100755 run-wired-tests.sh create mode 100644 typescript-sample/package-lock.json diff --git a/.npmignore b/.npmignore index 47d86ec..eeeb25e 100644 --- a/.npmignore +++ b/.npmignore @@ -3,4 +3,3 @@ manualtest bitbucket-pipelines.yml README*.md typescript-sample -run-wired-tests.sh \ No newline at end of file diff --git a/run-wired-tests.sh b/run-wired-tests.sh deleted file mode 100755 index 333fe49..0000000 --- a/run-wired-tests.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -WIREDTEST=1 npm test diff --git a/src/FSM.ts b/src/FSM.ts index 0963a83..4064628 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -1083,7 +1083,7 @@ export class KnxFSMConnection extends KnxFSM { }) } - Disconnect(cb: () => void): void { + Disconnect(cb?: () => void): void { if (this.state === 'connecting') { KnxLog.get().debug('Disconnecting directly') this.transition('uninitialized') diff --git a/typescript-sample/package-lock.json b/typescript-sample/package-lock.json new file mode 100644 index 0000000..a8352c9 --- /dev/null +++ b/typescript-sample/package-lock.json @@ -0,0 +1,453 @@ +{ + "name": "typescript-sample", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typescript-sample", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "knx": "../", + "tslint": "^5.1.0", + "typescript": "^2.2.2" + } + }, + "..": { + "version": "2.5.4", + "license": "MIT", + "dependencies": { + "binary-parser": "^2.2.1", + "binary-protocol": "0.0.0", + "ipaddr.js": "2.1.0", + "log-driver": "1.2.7", + "machina": "^4.0.2" + }, + "devDependencies": { + "@types/node": "^20.12.3", + "@types/tape": "^5.6.4", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", + "esbuild-register": "^3.5.0", + "eslint": "^8.57.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "tape": "^5.7.5", + "typescript": "^5.4.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/knx": { + "resolved": "..", + "link": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev" + } + }, + "node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/typescript-sample/test-toggle-onoff.ts b/typescript-sample/test-toggle-onoff.ts index 95ab9fa..349b0c4 100644 --- a/typescript-sample/test-toggle-onoff.ts +++ b/typescript-sample/test-toggle-onoff.ts @@ -3,11 +3,11 @@ * (C) 2016-2017 Elias Karakoulakis */ -import * as knx from 'knx' +import { Connection, Datapoint } from 'knx' var groupAddress = process.argv[2] -var connection = new knx.Connection({ +var connection = new Connection({ ipAddr: process.env.KNXGW, handlers: { connected: onConnected @@ -16,7 +16,7 @@ var connection = new knx.Connection({ async function onConnected() { console.log('Connected') - var dp = new knx.Datapoint({ + var dp = new Datapoint({ ga: groupAddress, dpt: 'DPT1.001' }, connection) From c6afdde0058eaf84942fb78a40e62666de4b8bef Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 16:16:42 +0200 Subject: [PATCH 12/36] fix: add dpts export in index --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 4e3ab1e..2370ed6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import Connection from './FSM' import Datapoint from './Datapoint' import Devices from './devices' import Log from './KnxLog' +import dpts from './dptlib' // do not use import here or package.json would be loaded twice // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -24,4 +25,4 @@ logger.info( ), ) -export { Connection, Datapoint, Devices, Log } +export { Connection, Datapoint, Devices, Log, dpts } From 366940eb9bf488ea1098d136cafb879240285f76 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 17:21:45 +0200 Subject: [PATCH 13/36] fix: missing types --- .eslintrc.js | 2 -- src/KnxConstants.ts | 8 +++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a26beca..1339fa2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,9 +38,7 @@ module.exports = { 'radix': 'off', 'func-names': 'off', // Typescript rules - '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/naming-convention': 'off', diff --git a/src/KnxConstants.ts b/src/KnxConstants.ts index b1adbf1..9499a80 100644 --- a/src/KnxConstants.ts +++ b/src/KnxConstants.ts @@ -77,7 +77,7 @@ const MESSAGECODES = { 'ETS.Dummy1': 0xc1, } -const APCICODES: string[] = [ +export const APCICODES: string[] = [ 'GroupValue_Read', 'GroupValue_Response', 'GroupValue_Write', @@ -96,7 +96,7 @@ const APCICODES: string[] = [ 'OTHER', ] -const KnxConstants = { +export const KnxConstants = { SERVICE_TYPE, CONNECTION_TYPE, PROTOCOL_TYPE, @@ -107,7 +107,7 @@ const KnxConstants = { } /* TODO helper function to print enum keys */ -const keyText = (mapref: string | object, value: number): string => { +export function keyText(mapref: string | object, value: number): string { const map = typeof mapref === 'string' ? KnxConstants[mapref] : mapref if (typeof map !== 'object') throw Error(`Unknown map: ${mapref}`) @@ -115,5 +115,3 @@ const keyText = (mapref: string | object, value: number): string => { KnxLog.get().trace('not found: %j', value) } - -export { KnxConstants, APCICODES, keyText } From 2e1eb24bec9fae02b9b3ade0eadea3e0b592c561 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 17:32:13 +0200 Subject: [PATCH 14/36] fix: add missing types --- src/FSM.ts | 114 +++++++++++++++++++++-------------------- src/types/machina.d.ts | 8 ++- 2 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/FSM.ts b/src/FSM.ts index 4064628..5e5a94c 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -10,7 +10,7 @@ import KnxNetProtocol from './KnxProtocol' import { Writer } from 'binary-protocol' import { Socket } from 'dgram' import { populateAPDU } from './dptlib' -import { LogLevel } from 'log-driver' +import { Logger, LogLevel } from 'log-driver' import { hasProp } from './utils' type KnxDeviceAddress = string @@ -120,19 +120,19 @@ const KnxFSM = machina.Fsm.extend({ initialState: 'uninitialized', states: { uninitialized: { - '*': function () { + '*': function (this: KnxFSMConnection) { this.transition('connecting') }, }, jumptoconnecting: { - _onEnter() { + _onEnter(this: KnxFSMConnection) { this.transition('connecting') }, }, connecting: { - _onEnter() { + _onEnter(this: KnxFSMConnection) { this.emit('disconnected') this.log.debug( util.format('useTunneling=%j', this.useTunneling), @@ -193,10 +193,10 @@ const KnxFSM = machina.Fsm.extend({ this.transition('connected') } }, - _onExit() { + _onExit(this: KnxFSMConnection) { clearInterval(this.connecttimer) }, - inbound_CONNECT_RESPONSE(datagram: any) { + inbound_CONNECT_RESPONSE(this: KnxFSMConnection, datagram: any) { this.log.debug(util.format('got connect response')) if ( hasProp(datagram, 'connstate') && @@ -225,7 +225,7 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { + inbound_CONNECTIONSTATE_RESPONSE(this: KnxFSMConnection, datagram: any) { if (this.useTunneling) { const str = keyText( 'RESPONSECODE', @@ -241,7 +241,7 @@ const KnxFSM = machina.Fsm.extend({ this.transition('connected') } }, - '*': function (data: any) { + '*': function (this: KnxFSMConnection, data: any) { this.log.debug( util.format('*** deferring Until Transition %j', data), ) @@ -250,7 +250,7 @@ const KnxFSM = machina.Fsm.extend({ }, connected: { - _onEnter() { + _onEnter(this: KnxFSMConnection) { this.reconnection_cycles = 0 this.seqnum = -1 this.lastSentTime = Date.now() @@ -267,7 +267,7 @@ const KnxFSM = machina.Fsm.extend({ }, disconnecting: { - _onEnter() { + _onEnter(this: KnxFSMConnection) { if (this.useTunneling) { const aliveFor = this.conntime ? Date.now() - this.conntime @@ -303,10 +303,10 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - _onExit() { + _onExit(this: KnxFSMConnection) { clearTimeout(this.disconnecttimer) }, - inbound_DISCONNECT_RESPONSE(datagram: any) { + inbound_DISCONNECT_RESPONSE(this: KnxFSMConnection, datagram: any) { if (this.useTunneling) { KnxLog.get().debug( '(%s):\tgot disconnect response', @@ -324,7 +324,7 @@ const KnxFSM = machina.Fsm.extend({ }, idle: { - _onEnter() { + _onEnter(this: KnxFSMConnection) { if (this.useTunneling) { if (this.idletimer == null) { this.idletimer = setTimeout(() => { @@ -341,8 +341,8 @@ const KnxFSM = machina.Fsm.extend({ ) this.processQueue() }, - _onExit() {}, - outbound_ROUTING_INDICATION(datagram: Datagram) { + _onExit(this: KnxFSMConnection) {}, + outbound_ROUTING_INDICATION(this: KnxFSMConnection, datagram: Datagram) { const elapsed = Date.now() - this.lastSentTime if ( !this.options.minimumDelay || @@ -360,7 +360,7 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - outbound_TUNNELING_REQUEST(datagram: Datagram) { + outbound_TUNNELING_REQUEST(this: KnxFSMConnection, datagram: Datagram) { if (this.useTunneling) { const elapsed = Date.now() - this.lastSentTime if ( @@ -386,14 +386,14 @@ const KnxFSM = machina.Fsm.extend({ } }, 'inbound_TUNNELING_REQUEST_L_Data.ind': function ( - datagram: Datagram, + this: KnxFSMConnection, datagram: Datagram, ) { if (this.useTunneling) { this.transition('recvTunnReqIndication', datagram) } }, 'inbound_TUNNELING_REQUEST_L_Data.con': function ( - datagram: Datagram, + this: KnxFSMConnection, datagram: Datagram, ) { if (this.useTunneling) { const confirmed = @@ -414,11 +414,11 @@ const KnxFSM = machina.Fsm.extend({ } }, 'inbound_ROUTING_INDICATION_L_Data.ind': function ( - datagram: Datagram, + this: KnxFSMConnection, datagram: Datagram, ) { this.emitEvent(datagram) }, - inbound_DISCONNECT_REQUEST(datagram: any) { + inbound_DISCONNECT_REQUEST(this: KnxFSMConnection, datagram: any) { if (this.useTunneling) { this.transition('connecting') } @@ -426,7 +426,7 @@ const KnxFSM = machina.Fsm.extend({ }, requestingConnState: { - _onEnter() { + _onEnter(this: KnxFSMConnection) { KnxLog.get().debug('Requesting Connection State') KnxLog.get().trace( '(%s): Requesting Connection State', @@ -444,10 +444,10 @@ const KnxFSM = machina.Fsm.extend({ this.emit('error', msg) }, 1000) }, - _onExit() { + _onExit(this: KnxFSMConnection) { clearTimeout(this.connstatetimer) }, - inbound_CONNECTIONSTATE_RESPONSE(datagram: any) { + inbound_CONNECTIONSTATE_RESPONSE(this: KnxFSMConnection, datagram: any) { const state = keyText('RESPONSECODE', datagram.connstate.status) switch (datagram.connstate.status) { case 0: @@ -465,7 +465,7 @@ const KnxFSM = machina.Fsm.extend({ this.emit('error', state) } }, - '*': function (data: any) { + '*': function (this: KnxFSMConnection, data: any) { this.log.debug( util.format( '*** deferring %s until transition from requestingConnState => idle', @@ -477,7 +477,7 @@ const KnxFSM = machina.Fsm.extend({ }, sendDatagram: { - _onEnter(datagram: Datagram) { + _onEnter(this: KnxFSMConnection, datagram: Datagram) { this.seqnum += 1 if (this.useTunneling) datagram.tunnstate.seqnum = this.seqnum & 0xff @@ -503,7 +503,7 @@ const KnxFSM = machina.Fsm.extend({ } }) }, - '*': function (data: any) { + '*': function (this: KnxFSMConnection, data: any) { this.log.debug( util.format( '*** deferring %s until transition sendDatagram => idle', @@ -514,17 +514,17 @@ const KnxFSM = machina.Fsm.extend({ }, }, sendTunnReq_waitACK: { - _onEnter(datagram: Datagram) { + _onEnter(this: KnxFSMConnection, datagram: Datagram) { this.tunnelingAckTimer = setTimeout(() => { this.log.debug('timed out waiting for TUNNELING_ACK') this.transition('idle') this.emit('tunnelreqfailed', datagram) }, 2000) }, - _onExit() { + _onExit(this: KnxFSMConnection) { clearTimeout(this.tunnelingAckTimer) }, - inbound_TUNNELING_ACK(datagram: Datagram) { + inbound_TUNNELING_ACK(this: KnxFSMConnection, datagram: Datagram) { this.log.debug( util.format( '===== datagram %d acknowledged by IP router', @@ -533,7 +533,7 @@ const KnxFSM = machina.Fsm.extend({ ) this.transition('idle') }, - '*': function (data: any) { + '*': function (this: KnxFSMConnection, data: any) { this.log.debug( util.format( '*** deferring %s until transition sendTunnReq_waitACK => idle', @@ -544,13 +544,13 @@ const KnxFSM = machina.Fsm.extend({ }, }, recvTunnReqIndication: { - _onEnter(datagram: Datagram) { + _onEnter(this: KnxFSMConnection, datagram: Datagram) { this.seqnumRecv = datagram.tunnstate.seqnum this.acknowledge(datagram) this.transition('idle') this.emitEvent(datagram) }, - '*': function (data: any) { + '*': function (this: KnxFSMConnection, data: any) { this.log.debug( util.format('*** deferring Until Transition %j', data), ) @@ -561,50 +561,54 @@ const KnxFSM = machina.Fsm.extend({ }) export class KnxFSMConnection extends KnxFSM { - private options: KnxOptions + protected options: KnxOptions - private log: any + protected log: Logger - private ThreeLevelGroupAddressing: boolean + protected ThreeLevelGroupAddressing: boolean - private reconnection_cycles: number + protected reconnection_cycles: number - private sentTunnRequests: { [key: string]: Datagram } + protected sentTunnRequests: { [key: string]: Datagram } - private useTunneling: boolean + protected useTunneling: boolean - private remoteEndpoint: { + protected remoteEndpoint: { addrstring: string addr: any port: number } - private localEchoInTunneling: boolean | undefined + protected localEchoInTunneling: boolean | undefined - private channel_id?: any + protected channel_id?: any - private conntime?: number + protected conntime?: number - private lastSentTime?: number + protected lastSentTime?: number - private connecttimer?: NodeJS.Timeout + protected connecttimer?: NodeJS.Timeout - private disconnecttimer?: NodeJS.Timeout + protected disconnecttimer?: NodeJS.Timeout - private connstatetimer?: NodeJS.Timeout + protected connstatetimer?: NodeJS.Timeout - private idletimer?: NodeJS.Timeout + protected idletimer?: NodeJS.Timeout - private tunnelingAckTimer?: NodeJS.Timeout + protected tunnelingAckTimer?: NodeJS.Timeout - private seqnum: number + protected seqnum: number + + protected seqnumRecv: number - private seqnumRecv: number - - private writer: Writer - - private socket: Socket + protected writer: Writer + protected socket: Socket + + protected usingMulticastTunneling: boolean + + protected minimumDelay: number + public localAddress: string | null constructor(options: KnxOptions) { @@ -936,7 +940,7 @@ export class KnxFSMConnection extends KnxFSM { /* send the datagram over the wire */ - send(datagram: Datagram, callback: (err?: Error) => void): void { + send(datagram: Datagram, callback?: (err?: Error) => void): void { let cemitype: string // TODO: set, but unused try { this.writer = KnxNetProtocol.createWriter() diff --git a/src/types/machina.d.ts b/src/types/machina.d.ts index acac96b..6bbef99 100644 --- a/src/types/machina.d.ts +++ b/src/types/machina.d.ts @@ -41,8 +41,10 @@ declare module 'machina' { compositeState(client: BehavioralFsm): any clearQueue(client: BehavioralFsm, name?: string): void handle(client: BehavioralFsm, ...args: any[]): any - transition(client: BehavioralFsm, newState: string): void + transition(client: BehavioralFsm, newState: string, ...args: any[]): void + deferUntilTransition(client: BehavioralFsm, state: string): void initialize(...args: any): void + processQueue(client: BehavioralFsm): void } export class Fsm extends BehavioralFsm { @@ -50,6 +52,8 @@ declare module 'machina' { compositeState(): any clearQueue(name?: string): void handle(...args: any[]): any - transition(newState: string): void + transition(newState: string, ...args: any[]): void + deferUntilTransition(state: string): void + processQueue(): void } } From 772218f800e74bc6b110e869311b0a25686751c3 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 17:33:08 +0200 Subject: [PATCH 15/36] fix: lint --- src/FSM.ts | 37 ++++++++++++++++++++++++++----------- src/types/machina.d.ts | 6 +++++- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/FSM.ts b/src/FSM.ts index 5e5a94c..45b7b73 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -225,7 +225,10 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - inbound_CONNECTIONSTATE_RESPONSE(this: KnxFSMConnection, datagram: any) { + inbound_CONNECTIONSTATE_RESPONSE( + this: KnxFSMConnection, + datagram: any, + ) { if (this.useTunneling) { const str = keyText( 'RESPONSECODE', @@ -342,7 +345,10 @@ const KnxFSM = machina.Fsm.extend({ this.processQueue() }, _onExit(this: KnxFSMConnection) {}, - outbound_ROUTING_INDICATION(this: KnxFSMConnection, datagram: Datagram) { + outbound_ROUTING_INDICATION( + this: KnxFSMConnection, + datagram: Datagram, + ) { const elapsed = Date.now() - this.lastSentTime if ( !this.options.minimumDelay || @@ -360,7 +366,10 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - outbound_TUNNELING_REQUEST(this: KnxFSMConnection, datagram: Datagram) { + outbound_TUNNELING_REQUEST( + this: KnxFSMConnection, + datagram: Datagram, + ) { if (this.useTunneling) { const elapsed = Date.now() - this.lastSentTime if ( @@ -386,14 +395,16 @@ const KnxFSM = machina.Fsm.extend({ } }, 'inbound_TUNNELING_REQUEST_L_Data.ind': function ( - this: KnxFSMConnection, datagram: Datagram, + this: KnxFSMConnection, + datagram: Datagram, ) { if (this.useTunneling) { this.transition('recvTunnReqIndication', datagram) } }, 'inbound_TUNNELING_REQUEST_L_Data.con': function ( - this: KnxFSMConnection, datagram: Datagram, + this: KnxFSMConnection, + datagram: Datagram, ) { if (this.useTunneling) { const confirmed = @@ -414,7 +425,8 @@ const KnxFSM = machina.Fsm.extend({ } }, 'inbound_ROUTING_INDICATION_L_Data.ind': function ( - this: KnxFSMConnection, datagram: Datagram, + this: KnxFSMConnection, + datagram: Datagram, ) { this.emitEvent(datagram) }, @@ -447,7 +459,10 @@ const KnxFSM = machina.Fsm.extend({ _onExit(this: KnxFSMConnection) { clearTimeout(this.connstatetimer) }, - inbound_CONNECTIONSTATE_RESPONSE(this: KnxFSMConnection, datagram: any) { + inbound_CONNECTIONSTATE_RESPONSE( + this: KnxFSMConnection, + datagram: any, + ) { const state = keyText('RESPONSECODE', datagram.connstate.status) switch (datagram.connstate.status) { case 0: @@ -598,17 +613,17 @@ export class KnxFSMConnection extends KnxFSM { protected tunnelingAckTimer?: NodeJS.Timeout protected seqnum: number - + protected seqnumRecv: number protected writer: Writer protected socket: Socket - + protected usingMulticastTunneling: boolean - + protected minimumDelay: number - + public localAddress: string | null constructor(options: KnxOptions) { diff --git a/src/types/machina.d.ts b/src/types/machina.d.ts index 6bbef99..3172018 100644 --- a/src/types/machina.d.ts +++ b/src/types/machina.d.ts @@ -41,7 +41,11 @@ declare module 'machina' { compositeState(client: BehavioralFsm): any clearQueue(client: BehavioralFsm, name?: string): void handle(client: BehavioralFsm, ...args: any[]): any - transition(client: BehavioralFsm, newState: string, ...args: any[]): void + transition( + client: BehavioralFsm, + newState: string, + ...args: any[] + ): void deferUntilTransition(client: BehavioralFsm, state: string): void initialize(...args: any): void processQueue(client: BehavioralFsm): void From f457341266eb5b52834bad69f349644f225ec7e4 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 17:36:19 +0200 Subject: [PATCH 16/36] fix: jsdocs --- src/FSM.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FSM.ts b/src/FSM.ts index 45b7b73..fe5307e 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -694,6 +694,7 @@ export class KnxFSMConnection extends KnxFSM { * -------------------------------- */ + /** ACK a datagram */ acknowledge(datagram: Datagram) { const ack = this.prepareDatagram( KnxConstants.SERVICE_TYPE.TUNNELING_ACK, From ccda6fe84cfafa1233459d8d63a40c7dbb7634c8 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 17:40:15 +0200 Subject: [PATCH 17/36] fix: js docs --- src/FSM.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/FSM.ts b/src/FSM.ts index fe5307e..e7d5404 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -847,15 +847,6 @@ export class KnxFSMConnection extends KnxFSM { } } - AddCRI = (datagram: Datagram): void => { - // add the CRI - datagram.cri = { - connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, - knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, - unused: 0, - } - } - AddCEMI(datagram: Datagram, msgcode?: number): void { const sendAck = (msgcode || 0x11) === 0x11 && !this.options.suppress_ack_ldatareq // only for L_Data.req @@ -927,7 +918,7 @@ export class KnxFSMConnection extends KnxFSM { switch (svcType) { case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: AddTunn(datagram) - this.AddCRI(datagram) // no break! + AddCRI(datagram) // no break! // eslint-disable-next-line no-fallthrough case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: @@ -953,9 +944,9 @@ export class KnxFSMConnection extends KnxFSM { return datagram } - /* - send the datagram over the wire - */ + /** + * Send the datagram over the wire + */ send(datagram: Datagram, callback?: (err?: Error) => void): void { let cemitype: string // TODO: set, but unused try { @@ -1074,8 +1065,10 @@ export class KnxFSMConnection extends KnxFSM { ) } - // send a READ request to the bus - // you can pass a callback function which gets bound to the RESPONSE datagram event + /** + * Send a READ request to the bus + * you can pass a callback function which gets bound to the RESPONSE datagram event + * */ read(grpaddr: string, callback: (src: any, data: any) => void): void { if (typeof callback === 'function') { // when the response arrives: @@ -1103,6 +1096,9 @@ export class KnxFSMConnection extends KnxFSM { }) } + /** + * Disconnect from the KNX bus + */ Disconnect(cb?: () => void): void { if (this.state === 'connecting') { KnxLog.get().debug('Disconnecting directly') @@ -1177,4 +1173,13 @@ const AddTunn = (datagram: Datagram): void => { } } +const AddCRI = (datagram: Datagram): void => { + // add the CRI + datagram.cri = { + connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, + knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, + unused: 0, + } +} + export default KnxFSMConnection From b8e38e569045e38fff528b3b0b6bbe7466eb0140 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 3 Apr 2024 17:43:10 +0200 Subject: [PATCH 18/36] fix: some docs --- src/FSM.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSM.ts b/src/FSM.ts index e7d5404..3b0ac3e 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -904,7 +904,7 @@ export class KnxFSMConnection extends KnxFSM { if (typeof callback === 'function') callback() } - // prepare a datagram for the given service type + /** prepare a datagram for the given service type */ prepareDatagram(svcType: number): Datagram { const datagram: Datagram = { header_length: 6, From 6fe22c88d06ef20b2547d01268698bfbc7fc91ce Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 4 Apr 2024 09:51:23 +0200 Subject: [PATCH 19/36] fix: split FSM and KnxClient --- src/FSM.ts | 803 ++--------------------------------- src/IpRoutingConnection.ts | 4 +- src/IpTunnelingConnection.ts | 4 +- src/KnxClient.ts | 725 +++++++++++++++++++++++++++++++ src/KnxProtocol.ts | 2 +- src/dptlib/index.ts | 2 +- src/index.ts | 4 +- test/dptlib/commontest.ts | 2 +- test/dptlib/test-dpt10.ts | 2 +- test/dptlib/test-dpt11.ts | 2 +- test/dptlib/test-dpt19.ts | 2 +- 11 files changed, 774 insertions(+), 778 deletions(-) create mode 100644 src/KnxClient.ts diff --git a/src/FSM.ts b/src/FSM.ts index 3b0ac3e..9762043 100644 --- a/src/FSM.ts +++ b/src/FSM.ts @@ -1,138 +1,28 @@ -import os from 'os' -import util from 'util' -import * as ipaddr from 'ipaddr.js' -import machina from 'machina' -import { keyText, KnxConstants } from './KnxConstants' -import IpRoutingConnection from './IpRoutingConnection' -import IpTunnelingConnection from './IpTunnelingConnection' -import KnxLog, { KnxLogOptions } from './KnxLog' -import KnxNetProtocol from './KnxProtocol' -import { Writer } from 'binary-protocol' -import { Socket } from 'dgram' -import { populateAPDU } from './dptlib' -import { Logger, LogLevel } from 'log-driver' +import type { KnxClient, Datagram } from './KnxClient' +import { KnxConstants, keyText } from './KnxConstants' +import KnxLog from './KnxLog' import { hasProp } from './utils' - -type KnxDeviceAddress = string - -type KnxGroupAddress = string - -/** The type of the KnxValue depends on the DPT that it is associated with */ -type KnxValue = number | string | boolean | Date - -type HandlersSpec = { - connected?: () => void - disconnected?: () => void - event?: ( - evt: string, - src: KnxDeviceAddress, - dest: KnxGroupAddress, - value: Buffer, - ) => void - error?: (connstatus: any) => void -} - -export type KnxOptions = { - /** ip address of the KNX router or interface */ - ipAddr?: string - /** port of the KNX router or interface */ - ipPort?: number - /** in case you need to specify the multicast interface (say if you have more than one) */ - interface?: string - /** the KNX physical address we'd like to use */ - physAddr?: string - /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ - loglevel?: LogLevel - /** do not automatically connect, but use connection.Connect() to establish connection */ - manualConnect?: boolean - /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ - forceTunneling?: boolean - /** wait at least 10 millisec between each datagram */ - minimumDelay?: number - /** enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this */ - suppress_ack_ldatareq?: boolean - /** 14/03/2020 In tunneling mode, echoes the sent message by emitting a new emitEvent, so other object with same group address, can receive the sent message. Default is false. */ - localEchoInTunneling?: boolean - /** event handlers. You can also bind them later with connection.on(event, fn) */ - handlers?: HandlersSpec -} & KnxLogOptions - -export interface Datagram { - header_length: number - protocol_version: number - service_type: number - total_length: number - cemi?: { - dest_addr: string - src_addr: string - addinfo_length?: number - apdu: { - apci: string - data: any - tpci: number - bitlength?: number - apdu_length?: number - apdu_raw?: any - } - msgcode: number - ctrl?: { - frameType: number - reserved: number - repeat: number - broadcast: number - priority: number - acknowledge: number - confirm: number - destAddrType: number - hopCount: number - extendedFrame: number - } - } - tunnstate?: { - seqnum?: number - channel_id: number - tunnel_endpoint: string - rsvd?: number - } - tunn?: { - protocol_type: number - tunnel_endpoint: string - } - hpai?: { - header_length?: number - protocol_type: number - tunnel_endpoint: string - } - connstate?: { - state: number - channel_id: number - status?: number - } - cri?: { - connection_type: number - knx_layer: number - unused: number - } -} +import machina from 'machina' +import util from 'util' const KnxFSM = machina.Fsm.extend({ namespace: 'knxnet', initialState: 'uninitialized', states: { uninitialized: { - '*': function (this: KnxFSMConnection) { + '*': function (this: KnxClient) { this.transition('connecting') }, }, jumptoconnecting: { - _onEnter(this: KnxFSMConnection) { + _onEnter(this: KnxClient) { this.transition('connecting') }, }, connecting: { - _onEnter(this: KnxFSMConnection) { + _onEnter(this: KnxClient) { this.emit('disconnected') this.log.debug( util.format('useTunneling=%j', this.useTunneling), @@ -193,10 +83,10 @@ const KnxFSM = machina.Fsm.extend({ this.transition('connected') } }, - _onExit(this: KnxFSMConnection) { + _onExit(this: KnxClient) { clearInterval(this.connecttimer) }, - inbound_CONNECT_RESPONSE(this: KnxFSMConnection, datagram: any) { + inbound_CONNECT_RESPONSE(this: KnxClient, datagram: any) { this.log.debug(util.format('got connect response')) if ( hasProp(datagram, 'connstate') && @@ -225,10 +115,7 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - inbound_CONNECTIONSTATE_RESPONSE( - this: KnxFSMConnection, - datagram: any, - ) { + inbound_CONNECTIONSTATE_RESPONSE(this: KnxClient, datagram: any) { if (this.useTunneling) { const str = keyText( 'RESPONSECODE', @@ -244,7 +131,7 @@ const KnxFSM = machina.Fsm.extend({ this.transition('connected') } }, - '*': function (this: KnxFSMConnection, data: any) { + '*': function (this: KnxClient, data: any) { this.log.debug( util.format('*** deferring Until Transition %j', data), ) @@ -253,7 +140,7 @@ const KnxFSM = machina.Fsm.extend({ }, connected: { - _onEnter(this: KnxFSMConnection) { + _onEnter(this: KnxClient) { this.reconnection_cycles = 0 this.seqnum = -1 this.lastSentTime = Date.now() @@ -270,7 +157,7 @@ const KnxFSM = machina.Fsm.extend({ }, disconnecting: { - _onEnter(this: KnxFSMConnection) { + _onEnter(this: KnxClient) { if (this.useTunneling) { const aliveFor = this.conntime ? Date.now() - this.conntime @@ -306,10 +193,10 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - _onExit(this: KnxFSMConnection) { + _onExit(this: KnxClient) { clearTimeout(this.disconnecttimer) }, - inbound_DISCONNECT_RESPONSE(this: KnxFSMConnection, datagram: any) { + inbound_DISCONNECT_RESPONSE(this: KnxClient, datagram: any) { if (this.useTunneling) { KnxLog.get().debug( '(%s):\tgot disconnect response', @@ -327,7 +214,7 @@ const KnxFSM = machina.Fsm.extend({ }, idle: { - _onEnter(this: KnxFSMConnection) { + _onEnter(this: KnxClient) { if (this.useTunneling) { if (this.idletimer == null) { this.idletimer = setTimeout(() => { @@ -344,11 +231,8 @@ const KnxFSM = machina.Fsm.extend({ ) this.processQueue() }, - _onExit(this: KnxFSMConnection) {}, - outbound_ROUTING_INDICATION( - this: KnxFSMConnection, - datagram: Datagram, - ) { + _onExit(this: KnxClient) {}, + outbound_ROUTING_INDICATION(this: KnxClient, datagram: Datagram) { const elapsed = Date.now() - this.lastSentTime if ( !this.options.minimumDelay || @@ -366,10 +250,7 @@ const KnxFSM = machina.Fsm.extend({ ) } }, - outbound_TUNNELING_REQUEST( - this: KnxFSMConnection, - datagram: Datagram, - ) { + outbound_TUNNELING_REQUEST(this: KnxClient, datagram: Datagram) { if (this.useTunneling) { const elapsed = Date.now() - this.lastSentTime if ( @@ -395,7 +276,7 @@ const KnxFSM = machina.Fsm.extend({ } }, 'inbound_TUNNELING_REQUEST_L_Data.ind': function ( - this: KnxFSMConnection, + this: KnxClient, datagram: Datagram, ) { if (this.useTunneling) { @@ -403,7 +284,7 @@ const KnxFSM = machina.Fsm.extend({ } }, 'inbound_TUNNELING_REQUEST_L_Data.con': function ( - this: KnxFSMConnection, + this: KnxClient, datagram: Datagram, ) { if (this.useTunneling) { @@ -425,12 +306,12 @@ const KnxFSM = machina.Fsm.extend({ } }, 'inbound_ROUTING_INDICATION_L_Data.ind': function ( - this: KnxFSMConnection, + this: KnxClient, datagram: Datagram, ) { this.emitEvent(datagram) }, - inbound_DISCONNECT_REQUEST(this: KnxFSMConnection, datagram: any) { + inbound_DISCONNECT_REQUEST(this: KnxClient, datagram: any) { if (this.useTunneling) { this.transition('connecting') } @@ -438,7 +319,7 @@ const KnxFSM = machina.Fsm.extend({ }, requestingConnState: { - _onEnter(this: KnxFSMConnection) { + _onEnter(this: KnxClient) { KnxLog.get().debug('Requesting Connection State') KnxLog.get().trace( '(%s): Requesting Connection State', @@ -456,13 +337,10 @@ const KnxFSM = machina.Fsm.extend({ this.emit('error', msg) }, 1000) }, - _onExit(this: KnxFSMConnection) { + _onExit(this: KnxClient) { clearTimeout(this.connstatetimer) }, - inbound_CONNECTIONSTATE_RESPONSE( - this: KnxFSMConnection, - datagram: any, - ) { + inbound_CONNECTIONSTATE_RESPONSE(this: KnxClient, datagram: any) { const state = keyText('RESPONSECODE', datagram.connstate.status) switch (datagram.connstate.status) { case 0: @@ -480,7 +358,7 @@ const KnxFSM = machina.Fsm.extend({ this.emit('error', state) } }, - '*': function (this: KnxFSMConnection, data: any) { + '*': function (this: KnxClient, data: any) { this.log.debug( util.format( '*** deferring %s until transition from requestingConnState => idle', @@ -492,7 +370,7 @@ const KnxFSM = machina.Fsm.extend({ }, sendDatagram: { - _onEnter(this: KnxFSMConnection, datagram: Datagram) { + _onEnter(this: KnxClient, datagram: Datagram) { this.seqnum += 1 if (this.useTunneling) datagram.tunnstate.seqnum = this.seqnum & 0xff @@ -518,7 +396,7 @@ const KnxFSM = machina.Fsm.extend({ } }) }, - '*': function (this: KnxFSMConnection, data: any) { + '*': function (this: KnxClient, data: any) { this.log.debug( util.format( '*** deferring %s until transition sendDatagram => idle', @@ -529,17 +407,17 @@ const KnxFSM = machina.Fsm.extend({ }, }, sendTunnReq_waitACK: { - _onEnter(this: KnxFSMConnection, datagram: Datagram) { + _onEnter(this: KnxClient, datagram: Datagram) { this.tunnelingAckTimer = setTimeout(() => { this.log.debug('timed out waiting for TUNNELING_ACK') this.transition('idle') this.emit('tunnelreqfailed', datagram) }, 2000) }, - _onExit(this: KnxFSMConnection) { + _onExit(this: KnxClient) { clearTimeout(this.tunnelingAckTimer) }, - inbound_TUNNELING_ACK(this: KnxFSMConnection, datagram: Datagram) { + inbound_TUNNELING_ACK(this: KnxClient, datagram: Datagram) { this.log.debug( util.format( '===== datagram %d acknowledged by IP router', @@ -548,7 +426,7 @@ const KnxFSM = machina.Fsm.extend({ ) this.transition('idle') }, - '*': function (this: KnxFSMConnection, data: any) { + '*': function (this: KnxClient, data: any) { this.log.debug( util.format( '*** deferring %s until transition sendTunnReq_waitACK => idle', @@ -559,13 +437,13 @@ const KnxFSM = machina.Fsm.extend({ }, }, recvTunnReqIndication: { - _onEnter(this: KnxFSMConnection, datagram: Datagram) { + _onEnter(this: KnxClient, datagram: Datagram) { this.seqnumRecv = datagram.tunnstate.seqnum this.acknowledge(datagram) this.transition('idle') this.emitEvent(datagram) }, - '*': function (this: KnxFSMConnection, data: any) { + '*': function (this: KnxClient, data: any) { this.log.debug( util.format('*** deferring Until Transition %j', data), ) @@ -575,611 +453,4 @@ const KnxFSM = machina.Fsm.extend({ }, }) -export class KnxFSMConnection extends KnxFSM { - protected options: KnxOptions - - protected log: Logger - - protected ThreeLevelGroupAddressing: boolean - - protected reconnection_cycles: number - - protected sentTunnRequests: { [key: string]: Datagram } - - protected useTunneling: boolean - - protected remoteEndpoint: { - addrstring: string - addr: any - port: number - } - - protected localEchoInTunneling: boolean | undefined - - protected channel_id?: any - - protected conntime?: number - - protected lastSentTime?: number - - protected connecttimer?: NodeJS.Timeout - - protected disconnecttimer?: NodeJS.Timeout - - protected connstatetimer?: NodeJS.Timeout - - protected idletimer?: NodeJS.Timeout - - protected tunnelingAckTimer?: NodeJS.Timeout - - protected seqnum: number - - protected seqnumRecv: number - - protected writer: Writer - - protected socket: Socket - - protected usingMulticastTunneling: boolean - - protected minimumDelay: number - - public localAddress: string | null - - constructor(options: KnxOptions) { - super() - - this.options = options || {} - this.log = KnxLog.get(options) - this.localAddress = null - this.ThreeLevelGroupAddressing = true - this.reconnection_cycles = 0 - this.sentTunnRequests = {} - this.useTunneling = options.forceTunneling || false - this.remoteEndpoint = { - addrstring: options.ipAddr || '224.0.23.12', - addr: ipaddr.parse(options.ipAddr || '224.0.23.12'), - port: options.ipPort || 3671, - } - const range = this.remoteEndpoint.addr.range() - this.localEchoInTunneling = - typeof options.localEchoInTunneling !== 'undefined' - ? options.localEchoInTunneling - : false - this.log.debug( - 'initializing %s connection to %s', - range, - this.remoteEndpoint.addrstring, - ) - switch (range) { - case 'multicast': - if (this.localEchoInTunneling) { - this.localEchoInTunneling = false - this.log.debug( - 'localEchoInTunneling: true but DISABLED because i am on multicast', - ) - } - IpRoutingConnection(this) - break - case 'unicast': - case 'private': - case 'loopback': - this.useTunneling = true - IpTunnelingConnection(this) - break - default: - throw Error( - util.format( - 'IP address % (%s) cannot be used for KNX', - options.ipAddr, - range, - ), - ) - } - - if (typeof options.handlers === 'object') { - for (const [key, value] of Object.entries(options.handlers)) { - if (typeof value === 'function') { - this.on(key, value) - } - } - } - // boot up the KNX connection unless told otherwise - if (!options.manualConnect) this.Connect() - } - - /** - * -------------------------------- - * KNX FSM Utils methods - * -------------------------------- - */ - - /** ACK a datagram */ - acknowledge(datagram: Datagram) { - const ack = this.prepareDatagram( - KnxConstants.SERVICE_TYPE.TUNNELING_ACK, - ) - ack.tunnstate.seqnum = datagram.tunnstate.seqnum - this.send(ack, (err: any) => {}) - } - - emitEvent(datagram: Datagram) { - const evtName = datagram.cemi.apdu.apci - this.emit( - util.format('event_%s', datagram.cemi.dest_addr), - evtName, - datagram.cemi.src_addr, - datagram.cemi.apdu.data, - ) - this.emit( - util.format('%s_%s', evtName, datagram.cemi.dest_addr), - datagram.cemi.src_addr, - datagram.cemi.apdu.data, - ) - this.emit( - evtName, - datagram.cemi.src_addr, - datagram.cemi.dest_addr, - datagram.cemi.apdu.data, - ) - this.emit( - 'event', - evtName, - datagram.cemi.src_addr, - datagram.cemi.dest_addr, - datagram.cemi.apdu.data, - ) - } - - getLocalAddress() { - const candidateInterfaces = this.getIPv4Interfaces() - if (this.options && this.options.interface) { - const iface = candidateInterfaces[this.options.interface] - if (!iface) - throw new Error( - `Interface ${this.options.interface} not found or has no useful IPv4 address!`, - ) - - return candidateInterfaces[this.options.interface].address - } - const first = Object.values(candidateInterfaces)[0] - if (first) return first.address - - throw Error('No valid IPv4 interfaces detected') - } - - getIPv4Interfaces() { - const candidateInterfaces: { [key: string]: any } = {} - const interfaces = os.networkInterfaces() - for (const [iface, addrs] of Object.entries(interfaces)) { - for (const addr of addrs) { - if ([4, 'IPv4'].indexOf(addr.family) > -1 && !addr.internal) { - this.log.trace( - util.format( - 'candidate interface: %s (%j)', - iface, - addr, - ), - ) - candidateInterfaces[iface] = addr - } - } - } - return candidateInterfaces - } - - BindSocket(cb: (socket: any) => void) { - // THIS IS A STUB and should be overridden by the connection type - } - - Connect() { - // THIS IS A STUB and should be overridden by the connection type - } - - /** - * -------------------------------- - * Connection management methods - * -------------------------------- - */ - - /** Bind incoming UDP packet handler */ - onUdpSocketMessage(msg: Buffer, rinfo: any, callback: () => void): void { - // get the incoming packet's service type ... - try { - const reader = KnxNetProtocol.createReader(msg) - // TODO: improve types for binary protocol - reader.KNXNetHeader('tmp') - const dg = reader.next()['tmp'] - const descr = datagramDesc(dg) - KnxLog.get().trace( - '(%s): Received %s message: %j', - this.compositeState(), - descr, - dg, - ) - if ( - !isNaN(this.channel_id) && - ((hasProp(dg, 'connstate') && - dg.connstate.channel_id !== this.channel_id) || - (hasProp(dg, 'tunnstate') && - dg.tunnstate.channel_id !== this.channel_id)) - ) { - KnxLog.get().trace( - '(%s): *** Ignoring %s datagram for other channel (own: %d)', - this.compositeState(), - descr, - this.channel_id, - ) - } else { - // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") - const signal = util.format('inbound_%s', descr) - if (descr === 'DISCONNECT_REQUEST') { - KnxLog.get().info( - 'empty internal fsm queue due to %s: ', - signal, - ) - this.clearQueue() - } - this.handle(signal, dg) - } - } catch (err) { - KnxLog.get().debug( - '(%s): Incomplete/unparseable UDP packet: %s: %s', - this.compositeState(), - err, - msg.toString('hex'), - ) - } - } - - AddConnState(datagram: Datagram): void { - datagram.connstate = { - channel_id: this.channel_id, - state: 0, - } - } - - AddTunnState(datagram: Datagram): void { - // add the remote IP router's endpoint - datagram.tunnstate = { - channel_id: this.channel_id, - tunnel_endpoint: `${this.remoteEndpoint.addr}:${this.remoteEndpoint.port}`, - } - } - - AddCEMI(datagram: Datagram, msgcode?: number): void { - const sendAck = - (msgcode || 0x11) === 0x11 && !this.options.suppress_ack_ldatareq // only for L_Data.req - datagram.cemi = { - msgcode: msgcode || 0x11, // default: L_Data.req for tunneling - ctrl: { - frameType: 1, // 0=extended 1=standard - reserved: 0, // always 0 - repeat: 1, // the OPPOSITE: 1=do NOT repeat - broadcast: 1, // 0-system broadcast 1-broadcast - priority: 3, // 0-system 1-normal 2-urgent 3-low - acknowledge: sendAck ? 1 : 0, - confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error - // 2nd byte - destAddrType: 1, // FIXME: 0-physical 1-groupaddr - hopCount: 6, - extendedFrame: 0, - }, - src_addr: this.options.physAddr || '15.15.15', - dest_addr: '0/0/0', // - apdu: { - // default operation is GroupValue_Write - apci: 'GroupValue_Write', - tpci: 0, - data: 0, - }, - } - } - - /* - * submit an outbound request to the state machine - * - * type: service type - * datagram_template: - * if a datagram is passed, use this as - * if a function is passed, use this to DECORATE - * if NULL, then just make a new empty datagram. Look at AddXXX methods - */ - Request( - type: number, - datagram_template: (datagram: Datagram) => void, - callback?: () => void, - ): void { - // populate skeleton datagram - const datagram = this.prepareDatagram(type) - // decorate the datagram, if a function is passed - if (typeof datagram_template === 'function') { - datagram_template(datagram) - } - // make sure that we override the datagram service type! - datagram.service_type = type - const st = keyText('SERVICE_TYPE', type) - // hand off the outbound request to the state machine - this.handle(`outbound_${st}`, datagram) - if (typeof callback === 'function') callback() - } - - /** prepare a datagram for the given service type */ - prepareDatagram(svcType: number): Datagram { - const datagram: Datagram = { - header_length: 6, - protocol_version: 16, // 0x10 == version 1.0 - service_type: svcType, - total_length: null, // filled in automatically - } - // - AddHPAI(datagram) - // - switch (svcType) { - case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: - AddTunn(datagram) - AddCRI(datagram) // no break! - // eslint-disable-next-line no-fallthrough - case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: - case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: - this.AddConnState(datagram) - break - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - this.AddCEMI(datagram, KnxConstants.MESSAGECODES['L_Data.ind']) - break - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - AddTunn(datagram) - this.AddTunnState(datagram) - this.AddCEMI(datagram) - break - case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: - this.AddTunnState(datagram) - break - default: - KnxLog.get().debug( - 'Do not know how to deal with svc type %d', - svcType, - ) - } - return datagram - } - - /** - * Send the datagram over the wire - */ - send(datagram: Datagram, callback?: (err?: Error) => void): void { - let cemitype: string // TODO: set, but unused - try { - this.writer = KnxNetProtocol.createWriter() - switch (datagram.service_type) { - case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: - // append the CEMI service type if this is a tunneling request... - cemitype = keyText('MESSAGECODES', datagram.cemi.msgcode) - break - } - const packet = this.writer.KNXNetHeader(datagram) - const buf = packet.buffer - const svctype = keyText('SERVICE_TYPE', datagram.service_type) // TODO: unused - const descr = datagramDesc(datagram) - KnxLog.get().trace( - '(%s): Sending %s ==> %j', - this.compositeState(), - descr, - datagram, - ) - this.socket.send( - buf, - 0, - buf.length, - this.remoteEndpoint.port, - this.remoteEndpoint.addr.toString(), - (err) => { - KnxLog.get().trace( - '(%s): UDP sent %s: %s %s', - this.compositeState(), - err ? err.toString() : 'OK', - descr, - buf.toString('hex'), - ) - if (typeof callback === 'function') callback(err) - }, - ) - } catch (e) { - KnxLog.get().warn(e) - if (typeof callback === 'function') callback(e) - } - } - - write( - grpaddr: string, - value: any, - dptid?: number, - callback?: () => void, - ): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn('You must supply both grpaddr and value!') - return - } - try { - // outbound request onto the state machine - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION - this.Request( - serviceType, - (datagram: Datagram) => { - populateAPDU(value, datagram.cemi.apdu, dptid) - datagram.cemi.dest_addr = grpaddr - }, - callback, - ) - } catch (e) { - KnxLog.get().warn(e) - } - } - - respond(grpaddr: string, value: any, dptid: number): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn('You must supply both grpaddr and value!') - return - } - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION - this.Request(serviceType, function (datagram: Datagram) { - populateAPDU(value, datagram.cemi.apdu, dptid) - // this is a READ request - datagram.cemi.apdu.apci = 'GroupValue_Response' - datagram.cemi.dest_addr = grpaddr - return datagram - }) - } - - writeRaw( - grpaddr: string, - value: Buffer, - bitlength?: number, - callback?: () => void, - ): void { - if (grpaddr == null || value == null) { - KnxLog.get().warn('You must supply both grpaddr and value!') - return - } - if (!Buffer.isBuffer(value)) { - KnxLog.get().warn('Value must be a buffer!') - return - } - // outbound request onto the state machine - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION - this.Request( - serviceType, - function (datagram: Datagram) { - datagram.cemi.apdu.data = value - datagram.cemi.apdu.bitlength = bitlength || value.byteLength * 8 - datagram.cemi.dest_addr = grpaddr - }, - callback, - ) - } - - /** - * Send a READ request to the bus - * you can pass a callback function which gets bound to the RESPONSE datagram event - * */ - read(grpaddr: string, callback: (src: any, data: any) => void): void { - if (typeof callback === 'function') { - // when the response arrives: - const responseEvent = `GroupValue_Response_${grpaddr}` - KnxLog.get().trace(`Binding connection to ${responseEvent}`) - const binding = (src: any, data: any) => { - // unbind the event handler - this.off(responseEvent, binding) - // fire the callback - callback(src, data) - } - // prepare for the response - this.on(responseEvent, binding) - // clean up after 3 seconds just in case no one answers the read request - setTimeout(() => this.off(responseEvent, binding), 3000) - } - const serviceType = this.useTunneling - ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST - : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION - this.Request(serviceType, function (datagram: Datagram) { - // this is a READ request - datagram.cemi.apdu.apci = 'GroupValue_Read' - datagram.cemi.dest_addr = grpaddr - return datagram - }) - } - - /** - * Disconnect from the KNX bus - */ - Disconnect(cb?: () => void): void { - if (this.state === 'connecting') { - KnxLog.get().debug('Disconnecting directly') - this.transition('uninitialized') - if (cb) { - cb() - } - return - } - - KnxLog.get().debug('waiting for Idle-State') - this.onIdle(() => { - KnxLog.get().trace('In Idle-State') - - this.on('disconnected', () => { - KnxLog.get().debug('Disconnected from KNX') - if (cb) { - cb() - } - }) - - KnxLog.get().debug('Disconnecting from KNX') - this.transition('disconnecting') - }) - - // machina.js removeAllListeners equivalent: - // this.off(); - } - - onIdle(cb: () => void): void { - if (this.state === 'idle') { - KnxLog.get().trace('Connection is already Idle') - cb() - } else { - this.on('transition', function (data: any) { - if (data.toState === 'idle') { - KnxLog.get().trace('Connection just transitioned to Idle') - cb() - } - }) - } - } -} - -// return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) -const datagramDesc = (dg: Datagram): string => { - let blurb = keyText('SERVICE_TYPE', dg.service_type) - if ( - dg.service_type === KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || - dg.service_type === KnxConstants.SERVICE_TYPE.ROUTING_INDICATION - ) { - blurb += `_${keyText('MESSAGECODES', dg.cemi.msgcode)}` - } - return blurb -} - -// add the control udp local endpoint. UPDATE: not needed apparnently? -const AddHPAI = (datagram: Datagram): void => { - datagram.hpai = { - protocol_type: 1, // UDP - // tunnel_endpoint: this.localAddress + ":" + this.control.address().port - tunnel_endpoint: '0.0.0.0:0', - } -} - -// add the tunneling udp local endpoint UPDATE: not needed apparently? -const AddTunn = (datagram: Datagram): void => { - datagram.tunn = { - protocol_type: 1, // UDP - tunnel_endpoint: '0.0.0.0:0', - // tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port - } -} - -const AddCRI = (datagram: Datagram): void => { - // add the CRI - datagram.cri = { - connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, - knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, - unused: 0, - } -} - -export default KnxFSMConnection +export default KnxFSM diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index 3dd2bab..9ea2096 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -1,9 +1,9 @@ import * as util from 'util' import * as dgram from 'dgram' import KnxLog from './KnxLog' -import type { KnxFSMConnection } from './FSM' +import type { KnxClient } from './KnxClient' -function IpRoutingConnection(instance: KnxFSMConnection): KnxFSMConnection { +function IpRoutingConnection(instance: KnxClient): KnxClient { const log = KnxLog.get() instance.BindSocket = function BindSocket( diff --git a/src/IpTunnelingConnection.ts b/src/IpTunnelingConnection.ts index d2996a7..28aa1f2 100644 --- a/src/IpTunnelingConnection.ts +++ b/src/IpTunnelingConnection.ts @@ -1,8 +1,8 @@ import dgram from 'dgram' import KnxLog from './KnxLog' -import type { KnxFSMConnection } from './FSM' +import type { KnxClient } from './KnxClient' -function IpTunnelingConnection(instance: KnxFSMConnection) { +function IpTunnelingConnection(instance: KnxClient) { const log = KnxLog.get() instance.BindSocket = function BindSocket( diff --git a/src/KnxClient.ts b/src/KnxClient.ts new file mode 100644 index 0000000..0e7b8f5 --- /dev/null +++ b/src/KnxClient.ts @@ -0,0 +1,725 @@ +import os from 'os' +import util from 'util' +import * as ipaddr from 'ipaddr.js' +import { keyText, KnxConstants } from './KnxConstants' +import IpRoutingConnection from './IpRoutingConnection' +import IpTunnelingConnection from './IpTunnelingConnection' +import KnxLog, { KnxLogOptions } from './KnxLog' +import KnxNetProtocol from './KnxProtocol' +import { Writer } from 'binary-protocol' +import { Socket } from 'dgram' +import { populateAPDU } from './dptlib' +import { Logger, LogLevel } from 'log-driver' +import { hasProp } from './utils' +import KnxFSM from './FSM' + +type KnxDeviceAddress = string + +type KnxGroupAddress = string + +/** The type of the KnxValue depends on the DPT that it is associated with */ +type KnxValue = number | string | boolean | Date + +type HandlersSpec = { + connected?: () => void + disconnected?: () => void + event?: ( + evt: string, + src: KnxDeviceAddress, + dest: KnxGroupAddress, + value: Buffer, + ) => void + error?: (connstatus: any) => void +} + +export type KnxOptions = { + /** ip address of the KNX router or interface */ + ipAddr?: string + /** port of the KNX router or interface */ + ipPort?: number + /** in case you need to specify the multicast interface (say if you have more than one) */ + interface?: string + /** the KNX physical address we'd like to use */ + physAddr?: string + /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ + loglevel?: LogLevel + /** do not automatically connect, but use connection.Connect() to establish connection */ + manualConnect?: boolean + /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ + forceTunneling?: boolean + /** wait at least 10 millisec between each datagram */ + minimumDelay?: number + /** enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this */ + suppress_ack_ldatareq?: boolean + /** 14/03/2020 In tunneling mode, echoes the sent message by emitting a new emitEvent, so other object with same group address, can receive the sent message. Default is false. */ + localEchoInTunneling?: boolean + /** event handlers. You can also bind them later with connection.on(event, fn) */ + handlers?: HandlersSpec +} & KnxLogOptions + +export interface Datagram { + header_length: number + protocol_version: number + service_type: number + total_length: number + cemi?: { + dest_addr: string + src_addr: string + addinfo_length?: number + apdu: { + apci: string + data: any + tpci: number + bitlength?: number + apdu_length?: number + apdu_raw?: any + } + msgcode: number + ctrl?: { + frameType: number + reserved: number + repeat: number + broadcast: number + priority: number + acknowledge: number + confirm: number + destAddrType: number + hopCount: number + extendedFrame: number + } + } + tunnstate?: { + seqnum?: number + channel_id: number + tunnel_endpoint: string + rsvd?: number + } + tunn?: { + protocol_type: number + tunnel_endpoint: string + } + hpai?: { + header_length?: number + protocol_type: number + tunnel_endpoint: string + } + connstate?: { + state: number + channel_id: number + status?: number + } + cri?: { + connection_type: number + knx_layer: number + unused: number + } +} + +export class KnxClient extends KnxFSM { + protected options: KnxOptions + + protected log: Logger + + protected ThreeLevelGroupAddressing: boolean + + protected reconnection_cycles: number + + protected sentTunnRequests: { [key: string]: Datagram } + + protected useTunneling: boolean + + protected remoteEndpoint: { + addrstring: string + addr: any + port: number + } + + protected localEchoInTunneling: boolean | undefined + + protected channel_id?: any + + protected conntime?: number + + protected lastSentTime?: number + + protected connecttimer?: NodeJS.Timeout + + protected disconnecttimer?: NodeJS.Timeout + + protected connstatetimer?: NodeJS.Timeout + + protected idletimer?: NodeJS.Timeout + + protected tunnelingAckTimer?: NodeJS.Timeout + + protected seqnum: number + + protected seqnumRecv: number + + protected writer: Writer + + protected socket: Socket + + protected usingMulticastTunneling: boolean + + protected minimumDelay: number + + public localAddress: string | null + + constructor(options: KnxOptions) { + super() + + this.options = options || {} + this.log = KnxLog.get(options) + this.localAddress = null + this.ThreeLevelGroupAddressing = true + this.reconnection_cycles = 0 + this.sentTunnRequests = {} + this.useTunneling = options.forceTunneling || false + this.remoteEndpoint = { + addrstring: options.ipAddr || '224.0.23.12', + addr: ipaddr.parse(options.ipAddr || '224.0.23.12'), + port: options.ipPort || 3671, + } + const range = this.remoteEndpoint.addr.range() + this.localEchoInTunneling = + typeof options.localEchoInTunneling !== 'undefined' + ? options.localEchoInTunneling + : false + this.log.debug( + 'initializing %s connection to %s', + range, + this.remoteEndpoint.addrstring, + ) + switch (range) { + case 'multicast': + if (this.localEchoInTunneling) { + this.localEchoInTunneling = false + this.log.debug( + 'localEchoInTunneling: true but DISABLED because i am on multicast', + ) + } + IpRoutingConnection(this) + break + case 'unicast': + case 'private': + case 'loopback': + this.useTunneling = true + IpTunnelingConnection(this) + break + default: + throw Error( + util.format( + 'IP address % (%s) cannot be used for KNX', + options.ipAddr, + range, + ), + ) + } + + if (typeof options.handlers === 'object') { + for (const [key, value] of Object.entries(options.handlers)) { + if (typeof value === 'function') { + this.on(key, value) + } + } + } + // boot up the KNX connection unless told otherwise + if (!options.manualConnect) this.Connect() + } + + /** + * -------------------------------- + * KNX FSM Utils methods + * -------------------------------- + */ + + /** ACK a datagram */ + acknowledge(datagram: Datagram) { + const ack = this.prepareDatagram( + KnxConstants.SERVICE_TYPE.TUNNELING_ACK, + ) + ack.tunnstate.seqnum = datagram.tunnstate.seqnum + this.send(ack, (err: any) => {}) + } + + emitEvent(datagram: Datagram) { + const evtName = datagram.cemi.apdu.apci + this.emit( + util.format('event_%s', datagram.cemi.dest_addr), + evtName, + datagram.cemi.src_addr, + datagram.cemi.apdu.data, + ) + this.emit( + util.format('%s_%s', evtName, datagram.cemi.dest_addr), + datagram.cemi.src_addr, + datagram.cemi.apdu.data, + ) + this.emit( + evtName, + datagram.cemi.src_addr, + datagram.cemi.dest_addr, + datagram.cemi.apdu.data, + ) + this.emit( + 'event', + evtName, + datagram.cemi.src_addr, + datagram.cemi.dest_addr, + datagram.cemi.apdu.data, + ) + } + + getLocalAddress() { + const candidateInterfaces = this.getIPv4Interfaces() + if (this.options && this.options.interface) { + const iface = candidateInterfaces[this.options.interface] + if (!iface) + throw new Error( + `Interface ${this.options.interface} not found or has no useful IPv4 address!`, + ) + + return candidateInterfaces[this.options.interface].address + } + const first = Object.values(candidateInterfaces)[0] + if (first) return first.address + + throw Error('No valid IPv4 interfaces detected') + } + + getIPv4Interfaces() { + const candidateInterfaces: { [key: string]: any } = {} + const interfaces = os.networkInterfaces() + for (const [iface, addrs] of Object.entries(interfaces)) { + for (const addr of addrs) { + if ([4, 'IPv4'].indexOf(addr.family) > -1 && !addr.internal) { + this.log.trace( + util.format( + 'candidate interface: %s (%j)', + iface, + addr, + ), + ) + candidateInterfaces[iface] = addr + } + } + } + return candidateInterfaces + } + + BindSocket(cb: (socket: any) => void) { + // THIS IS A STUB and should be overridden by the connection type + } + + Connect() { + // THIS IS A STUB and should be overridden by the connection type + } + + /** + * -------------------------------- + * Connection management methods + * -------------------------------- + */ + + /** Bind incoming UDP packet handler */ + onUdpSocketMessage(msg: Buffer, rinfo: any, callback: () => void): void { + // get the incoming packet's service type ... + try { + const reader = KnxNetProtocol.createReader(msg) + // TODO: improve types for binary protocol + reader.KNXNetHeader('tmp') + const dg = reader.next()['tmp'] + const descr = datagramDesc(dg) + KnxLog.get().trace( + '(%s): Received %s message: %j', + this.compositeState(), + descr, + dg, + ) + if ( + !isNaN(this.channel_id) && + ((hasProp(dg, 'connstate') && + dg.connstate.channel_id !== this.channel_id) || + (hasProp(dg, 'tunnstate') && + dg.tunnstate.channel_id !== this.channel_id)) + ) { + KnxLog.get().trace( + '(%s): *** Ignoring %s datagram for other channel (own: %d)', + this.compositeState(), + descr, + this.channel_id, + ) + } else { + // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") + const signal = util.format('inbound_%s', descr) + if (descr === 'DISCONNECT_REQUEST') { + KnxLog.get().info( + 'empty internal fsm queue due to %s: ', + signal, + ) + this.clearQueue() + } + this.handle(signal, dg) + } + } catch (err) { + KnxLog.get().debug( + '(%s): Incomplete/unparseable UDP packet: %s: %s', + this.compositeState(), + err, + msg.toString('hex'), + ) + } + } + + AddConnState(datagram: Datagram): void { + datagram.connstate = { + channel_id: this.channel_id, + state: 0, + } + } + + AddTunnState(datagram: Datagram): void { + // add the remote IP router's endpoint + datagram.tunnstate = { + channel_id: this.channel_id, + tunnel_endpoint: `${this.remoteEndpoint.addr}:${this.remoteEndpoint.port}`, + } + } + + AddCEMI(datagram: Datagram, msgcode?: number): void { + const sendAck = + (msgcode || 0x11) === 0x11 && !this.options.suppress_ack_ldatareq // only for L_Data.req + datagram.cemi = { + msgcode: msgcode || 0x11, // default: L_Data.req for tunneling + ctrl: { + frameType: 1, // 0=extended 1=standard + reserved: 0, // always 0 + repeat: 1, // the OPPOSITE: 1=do NOT repeat + broadcast: 1, // 0-system broadcast 1-broadcast + priority: 3, // 0-system 1-normal 2-urgent 3-low + acknowledge: sendAck ? 1 : 0, + confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error + // 2nd byte + destAddrType: 1, // FIXME: 0-physical 1-groupaddr + hopCount: 6, + extendedFrame: 0, + }, + src_addr: this.options.physAddr || '15.15.15', + dest_addr: '0/0/0', // + apdu: { + // default operation is GroupValue_Write + apci: 'GroupValue_Write', + tpci: 0, + data: 0, + }, + } + } + + /* + * submit an outbound request to the state machine + * + * type: service type + * datagram_template: + * if a datagram is passed, use this as + * if a function is passed, use this to DECORATE + * if NULL, then just make a new empty datagram. Look at AddXXX methods + */ + Request( + type: number, + datagram_template: (datagram: Datagram) => void, + callback?: () => void, + ): void { + // populate skeleton datagram + const datagram = this.prepareDatagram(type) + // decorate the datagram, if a function is passed + if (typeof datagram_template === 'function') { + datagram_template(datagram) + } + // make sure that we override the datagram service type! + datagram.service_type = type + const st = keyText('SERVICE_TYPE', type) + // hand off the outbound request to the state machine + this.handle(`outbound_${st}`, datagram) + if (typeof callback === 'function') callback() + } + + /** prepare a datagram for the given service type */ + prepareDatagram(svcType: number): Datagram { + const datagram: Datagram = { + header_length: 6, + protocol_version: 16, // 0x10 == version 1.0 + service_type: svcType, + total_length: null, // filled in automatically + } + // + AddHPAI(datagram) + // + switch (svcType) { + case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: + AddTunn(datagram) + AddCRI(datagram) // no break! + // eslint-disable-next-line no-fallthrough + case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: + case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: + this.AddConnState(datagram) + break + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + this.AddCEMI(datagram, KnxConstants.MESSAGECODES['L_Data.ind']) + break + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + AddTunn(datagram) + this.AddTunnState(datagram) + this.AddCEMI(datagram) + break + case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: + this.AddTunnState(datagram) + break + default: + KnxLog.get().debug( + 'Do not know how to deal with svc type %d', + svcType, + ) + } + return datagram + } + + /** + * Send the datagram over the wire + */ + send(datagram: Datagram, callback?: (err?: Error) => void): void { + let cemitype: string // TODO: set, but unused + try { + this.writer = KnxNetProtocol.createWriter() + switch (datagram.service_type) { + case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: + case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: + // append the CEMI service type if this is a tunneling request... + cemitype = keyText('MESSAGECODES', datagram.cemi.msgcode) + break + } + const packet = this.writer.KNXNetHeader(datagram) + const buf = packet.buffer + const svctype = keyText('SERVICE_TYPE', datagram.service_type) // TODO: unused + const descr = datagramDesc(datagram) + KnxLog.get().trace( + '(%s): Sending %s ==> %j', + this.compositeState(), + descr, + datagram, + ) + this.socket.send( + buf, + 0, + buf.length, + this.remoteEndpoint.port, + this.remoteEndpoint.addr.toString(), + (err) => { + KnxLog.get().trace( + '(%s): UDP sent %s: %s %s', + this.compositeState(), + err ? err.toString() : 'OK', + descr, + buf.toString('hex'), + ) + if (typeof callback === 'function') callback(err) + }, + ) + } catch (e) { + KnxLog.get().warn(e) + if (typeof callback === 'function') callback(e) + } + } + + write( + grpaddr: string, + value: any, + dptid?: number, + callback?: () => void, + ): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn('You must supply both grpaddr and value!') + return + } + try { + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request( + serviceType, + (datagram: Datagram) => { + populateAPDU(value, datagram.cemi.apdu, dptid) + datagram.cemi.dest_addr = grpaddr + }, + callback, + ) + } catch (e) { + KnxLog.get().warn(e) + } + } + + respond(grpaddr: string, value: any, dptid: number): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn('You must supply both grpaddr and value!') + return + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request(serviceType, function (datagram: Datagram) { + populateAPDU(value, datagram.cemi.apdu, dptid) + // this is a READ request + datagram.cemi.apdu.apci = 'GroupValue_Response' + datagram.cemi.dest_addr = grpaddr + return datagram + }) + } + + writeRaw( + grpaddr: string, + value: Buffer, + bitlength?: number, + callback?: () => void, + ): void { + if (grpaddr == null || value == null) { + KnxLog.get().warn('You must supply both grpaddr and value!') + return + } + if (!Buffer.isBuffer(value)) { + KnxLog.get().warn('Value must be a buffer!') + return + } + // outbound request onto the state machine + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request( + serviceType, + function (datagram: Datagram) { + datagram.cemi.apdu.data = value + datagram.cemi.apdu.bitlength = bitlength || value.byteLength * 8 + datagram.cemi.dest_addr = grpaddr + }, + callback, + ) + } + + /** + * Send a READ request to the bus + * you can pass a callback function which gets bound to the RESPONSE datagram event + * */ + read(grpaddr: string, callback: (src: any, data: any) => void): void { + if (typeof callback === 'function') { + // when the response arrives: + const responseEvent = `GroupValue_Response_${grpaddr}` + KnxLog.get().trace(`Binding connection to ${responseEvent}`) + const binding = (src: any, data: any) => { + // unbind the event handler + this.off(responseEvent, binding) + // fire the callback + callback(src, data) + } + // prepare for the response + this.on(responseEvent, binding) + // clean up after 3 seconds just in case no one answers the read request + setTimeout(() => this.off(responseEvent, binding), 3000) + } + const serviceType = this.useTunneling + ? KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST + : KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + this.Request(serviceType, function (datagram: Datagram) { + // this is a READ request + datagram.cemi.apdu.apci = 'GroupValue_Read' + datagram.cemi.dest_addr = grpaddr + return datagram + }) + } + + /** + * Disconnect from the KNX bus + */ + Disconnect(cb?: () => void): void { + if (this.state === 'connecting') { + KnxLog.get().debug('Disconnecting directly') + this.transition('uninitialized') + if (cb) { + cb() + } + return + } + + KnxLog.get().debug('waiting for Idle-State') + this.onIdle(() => { + KnxLog.get().trace('In Idle-State') + + this.on('disconnected', () => { + KnxLog.get().debug('Disconnected from KNX') + if (cb) { + cb() + } + }) + + KnxLog.get().debug('Disconnecting from KNX') + this.transition('disconnecting') + }) + + // machina.js removeAllListeners equivalent: + // this.off(); + } + + onIdle(cb: () => void): void { + if (this.state === 'idle') { + KnxLog.get().trace('Connection is already Idle') + cb() + } else { + this.on('transition', function (data: any) { + if (data.toState === 'idle') { + KnxLog.get().trace('Connection just transitioned to Idle') + cb() + } + }) + } + } +} + +// return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) +const datagramDesc = (dg: Datagram): string => { + let blurb = keyText('SERVICE_TYPE', dg.service_type) + if ( + dg.service_type === KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || + dg.service_type === KnxConstants.SERVICE_TYPE.ROUTING_INDICATION + ) { + blurb += `_${keyText('MESSAGECODES', dg.cemi.msgcode)}` + } + return blurb +} + +// add the control udp local endpoint. UPDATE: not needed apparnently? +const AddHPAI = (datagram: Datagram): void => { + datagram.hpai = { + protocol_type: 1, // UDP + // tunnel_endpoint: this.localAddress + ":" + this.control.address().port + tunnel_endpoint: '0.0.0.0:0', + } +} + +// add the tunneling udp local endpoint UPDATE: not needed apparently? +const AddTunn = (datagram: Datagram): void => { + datagram.tunn = { + protocol_type: 1, // UDP + tunnel_endpoint: '0.0.0.0:0', + // tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port + } +} + +const AddCRI = (datagram: Datagram): void => { + // add the CRI + datagram.cri = { + connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, + knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, + unused: 0, + } +} + +export default KnxClient diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index 0ec1513..e6f3b86 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -10,7 +10,7 @@ import BinaryProtocol from 'binary-protocol' import * as KnxAddress from './Address' import { APCICODES, keyText, KnxConstants } from './KnxConstants' import KnxLog from './KnxLog' -import type { Datagram } from './FSM' +import type { Datagram } from './KnxClient' export interface KnxProtocolI extends BinaryProtocol { lengths: { [key: string]: (value: any) => number } diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts index 76d09b8..6254b90 100644 --- a/src/dptlib/index.ts +++ b/src/dptlib/index.ts @@ -62,7 +62,7 @@ import DPT21 from './dpt21' import DPT232 from './dpt232' import DPT237 from './dpt237' import DPT238 from './dpt238' -import { Datagram } from 'src/FSM' +import { Datagram } from 'src/KnxClient' import { hasProp } from 'src/utils' const log = KnxLog.get() diff --git a/src/index.ts b/src/index.ts index 2370ed6..b684418 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { format } from 'util' import { logger } from 'log-driver' -import Connection from './FSM' +import { KnxClient } from './KnxClient' import Datapoint from './Datapoint' import Devices from './devices' import Log from './KnxLog' @@ -25,4 +25,4 @@ logger.info( ), ) -export { Connection, Datapoint, Devices, Log, dpts } +export { KnxClient, Datapoint, Devices, Log, dpts } diff --git a/test/dptlib/commontest.ts b/test/dptlib/commontest.ts index d41295f..b6b9b6f 100644 --- a/test/dptlib/commontest.ts +++ b/test/dptlib/commontest.ts @@ -6,7 +6,7 @@ import test, { Test } from 'tape' import DPTLib, { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' -import { Datagram } from '../../src/FSM' +import { Datagram } from '../../src/KnxClient' /* common DPT unit test. Tries to - 1. marshal a JS value into an apdu.data (Buffer) and compare it output to the expected value diff --git a/test/dptlib/test-dpt10.ts b/test/dptlib/test-dpt10.ts index e25d415..e137daf 100644 --- a/test/dptlib/test-dpt10.ts +++ b/test/dptlib/test-dpt10.ts @@ -5,7 +5,7 @@ import test from 'tape' import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' -import { Datagram } from 'src/FSM' +import { Datagram } from 'src/KnxClient' function timecompare(date1, sign, date2) { const dow1 = date1.getDay() diff --git a/test/dptlib/test-dpt11.ts b/test/dptlib/test-dpt11.ts index f801526..d0bf6c9 100644 --- a/test/dptlib/test-dpt11.ts +++ b/test/dptlib/test-dpt11.ts @@ -5,7 +5,7 @@ import test from 'tape' import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' -import { Datagram } from 'src/FSM' +import { Datagram } from 'src/KnxClient' function dateequals(d1: Date, d2: Date) { const d = d1.getDate() diff --git a/test/dptlib/test-dpt19.ts b/test/dptlib/test-dpt19.ts index c38694c..ed9b1e4 100644 --- a/test/dptlib/test-dpt19.ts +++ b/test/dptlib/test-dpt19.ts @@ -5,7 +5,7 @@ import test from 'tape' import DPTLib, { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' -import { Datagram } from 'src/FSM' +import { Datagram } from 'src/KnxClient' test('DPT19 datetime conversion', function (t) { const tests = ['1995-12-17T03:24:00', '1996-07-17T03:24:00'] From 8270a940f7590aa5150657688e50debb2a3e0e07 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 4 Apr 2024 10:25:49 +0200 Subject: [PATCH 20/36] fix: docs and manual tests --- README-API.md | 11 +++++++---- README-resilience.md | 3 +-- README.md | 14 +++++++------- manualtest/{test-toggle.js => test-toggle.ts} | 8 ++++---- .../{test-writestorm.js => test-writestorm.ts} | 8 ++++---- test/connection/test-connect-routing.ts | 4 ++-- test/wiredtests/test-connect-routing-hybrid.ts | 4 ++-- test/wiredtests/test-connect-tunnel.ts | 4 ++-- test/wiredtests/test-control.ts | 4 ++-- test/wiredtests/test-logotimer.ts | 4 ++-- test/wiredtests/test-read.ts | 4 ++-- test/wiredtests/test-readstorm.ts | 4 ++-- typescript-sample/test-toggle-onoff.ts | 4 ++-- 13 files changed, 39 insertions(+), 37 deletions(-) rename manualtest/{test-toggle.js => test-toggle.ts} (89%) rename manualtest/{test-writestorm.js => test-writestorm.ts} (83%) diff --git a/README-API.md b/README-API.md index 9e9df4b..e1b5f46 100644 --- a/README-API.md +++ b/README-API.md @@ -2,9 +2,10 @@ By default *you only need to specify a 'handlers' object* containing your functions to handle KNX events. All the other options have defaults that can be overridden according to your needs. - ```js -var connection = new knx.Connection( { +var { KnxClient, DataPoint } = require('knx'); + +var connection = new KnxClient( { // ip address and port of the KNX router or interface ipAddr: '127.0.0.1', ipPort: 3671, // in case you need to specify the multicast interface (say if you have more than one) @@ -83,13 +84,15 @@ you start defining datapoints (and devices as we'll see later), your code *needs to ensure that the connection has been established*, usually by declaring them in the 'connected' handler: ```js -var connection = knx.Connection({ +var { KnxClient, DataPoint } = require('knx'); + +var connection = new KnxClient({ handlers: { connected: function() { console.log('----------'); console.log('Connected!'); console.log('----------'); - var dp = new knx.Datapoint({ga: '1/1/1'}, connection); + var dp = new Datapoint({ga: '1/1/1'}, connection); // Now send off a couple of requests: dp.read((src, value) => { console.log("**** RESPONSE %j reports current value: %j", src, value); diff --git a/README-resilience.md b/README-resilience.md index c3a302e..37200b1 100644 --- a/README-resilience.md +++ b/README-resilience.md @@ -18,7 +18,6 @@ This library is, to the best of my knowledge, the only one that can handle the * 27 Oct 15:45:36 - [info] [knx-in:input] GroupValue_Read {"srcphy":"15.15.15","dstgad":"0/0/15"} ``` - ## A note on resilience There are basically *two* ways to talk to KNX via UDP/IP: @@ -30,7 +29,7 @@ There are basically *two* ways to talk to KNX via UDP/IP: - Finally, this library allows a **hybrid** approach, that's taking the best of the two methods above: You can use **multicast** transport with a **tunnelling** connection to ensure reliable communication. *Unfortunately this deviates from the official KNXnet/IP spec*, and is therefore not compatible with some IP routers. You can enable this "hybrid mode" by enabling the `forceTunneling` option when constructing a new Connection object as follows: ```js -var connection = new knx.Connection( { +var connection = new knx.KnxClient( { // use tunneling with multicast - this is NOT supported by all routers! forceTunneling: true, ... diff --git a/README.md b/README.md index 06c0906..7f6fa21 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Make sure your machine has Node.JS (version 4.x or greater) and do: At last, here's a **reliable** KNX connection that simply works without any configs. To get a basic KNX monitor, you just need to run this in Node: ```js -var knx = require('knx'); -var connection = knx.Connection({ +var { KnxClient } = require('knx'); +var connection = new KnxClient({ handlers: { connected: function() { console.log('Connected!'); @@ -49,8 +49,8 @@ Ahhh, KNX telegrams, what a joy: ## Development documentation -- [Basic API usage](../master/README-API.md) -- [List of supported datapoints](../master/README-datapoints.md) -- [List of supported events](../master/README-events.md) -- [eibd/knxd compatibility](../master/README-knxd.md) -- [On resilience](../master/README-resilience.md) +* [Basic API usage](../master/README-API.md) +* [List of supported datapoints](../master/README-datapoints.md) +* [List of supported events](../master/README-events.md) +* [eibd/knxd compatibility](../master/README-knxd.md) +* [On resilience](../master/README-resilience.md) diff --git a/manualtest/test-toggle.js b/manualtest/test-toggle.ts similarity index 89% rename from manualtest/test-toggle.js rename to manualtest/test-toggle.ts index bcaedf4..c7e7e95 100644 --- a/manualtest/test-toggle.js +++ b/manualtest/test-toggle.ts @@ -3,14 +3,14 @@ * (C) 2016-2017 Elias Karakoulakis */ -var knx = require('knx'); +import { Datapoint, KnxClient } from "src"; if (process.argv.length < 3) { console.log('usage: %s to toggle a light on & off', process.argv[1]); process.exit(1); } -var connection = knx.Connection({ +const connection = new KnxClient({ debug: true, handlers: { connected: function() { @@ -18,12 +18,12 @@ var connection = knx.Connection({ console.log('Connected!'); console.log('----------'); // define a datapoint: - var dp = new knx.Datapoint({ + var dp = new Datapoint({ ga: process.argv[2], dpt: 'DPT1.001' }, connection); if (process.argv[3]) { - var status_ga = new knx.Datapoint({ + var status_ga = new Datapoint({ ga: process.argv[3], dpt: 'DPT1.001' }, connection); diff --git a/manualtest/test-writestorm.js b/manualtest/test-writestorm.ts similarity index 83% rename from manualtest/test-writestorm.js rename to manualtest/test-writestorm.ts index 1cc180b..a9300a6 100644 --- a/manualtest/test-writestorm.js +++ b/manualtest/test-writestorm.ts @@ -3,7 +3,7 @@ * (C) 2016-2017 Elias Karakoulakis */ Error.stackTraceLimit = Infinity; -var knx = require('knx'); +import { Devices, KnxClient } from "src"; if (process.argv.length < 2) { console.log('usage: %s <0/1> (off/on) to write to a set of binary switches', process.argv[1]); @@ -11,14 +11,14 @@ if (process.argv.length < 2) { } function setupSwitch(groupaddress, statusga) { - var sw = new knx.Devices.BinarySwitch({ga: groupaddress, status_ga: statusga}, connection); - sw.on('change', (oldvalue, newvalue, ga) => { + var sw = new Devices.BinarySwitch({ga: groupaddress, status_ga: statusga}, connection); + sw.on('change', (oldvalue: boolean, newvalue: boolean, ga: string) => { console.log(" %s: **** %s current value: %j", Date.now(), ga, newvalue); }); return sw; } -var connection = knx.Connection({ +var connection = new KnxClient({ //debug: true, //minimumDelay: 10, handlers: { diff --git a/test/connection/test-connect-routing.ts b/test/connection/test-connect-routing.ts index 22001a8..4282480 100644 --- a/test/connection/test-connect-routing.ts +++ b/test/connection/test-connect-routing.ts @@ -3,14 +3,14 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { Connection } from '../../src' +import { KnxClient } from '../../src' import test from 'tape' Error.stackTraceLimit = Infinity // test('KNX connect routing', function (t) { - const connection = new Connection({ + const connection = new KnxClient({ loglevel: 'trace', handlers: { connected() { diff --git a/test/wiredtests/test-connect-routing-hybrid.ts b/test/wiredtests/test-connect-routing-hybrid.ts index d44e2fd..21587e1 100644 --- a/test/wiredtests/test-connect-routing-hybrid.ts +++ b/test/wiredtests/test-connect-routing-hybrid.ts @@ -2,7 +2,7 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import { Connection } from '../../src' +import { KnxClient } from '../../src' import test from 'tape' Error.stackTraceLimit = Infinity @@ -14,7 +14,7 @@ Error.stackTraceLimit = Infinity */ // test('KNX connect routing hybrid', function (t) { - const connection = new Connection({ + const connection = new KnxClient({ loglevel: 'debug', forceTunneling: true, handlers: { diff --git a/test/wiredtests/test-connect-tunnel.ts b/test/wiredtests/test-connect-tunnel.ts index 71ed3bc..29ad5a6 100644 --- a/test/wiredtests/test-connect-tunnel.ts +++ b/test/wiredtests/test-connect-tunnel.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { Connection } from '../../src' +import { KnxClient } from '../../src' import test from 'tape' import options from './wiredtest-options' @@ -17,7 +17,7 @@ Error.stackTraceLimit = Infinity */ // test('KNX connect tunneling', function (t) { - const connection = new Connection({ + const connection = new KnxClient({ // set up your KNX IP router's IP address (not multicast!) // for getting into tunnelling mode ipAddr: options.ipAddr, diff --git a/test/wiredtests/test-control.ts b/test/wiredtests/test-control.ts index 57865eb..47253cf 100644 --- a/test/wiredtests/test-control.ts +++ b/test/wiredtests/test-control.ts @@ -2,7 +2,7 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint } from '../../src' +import { KnxClient, Datapoint } from '../../src' import test from 'tape' import options from './wiredtest-options' @@ -16,7 +16,7 @@ Error.stackTraceLimit = Infinity test('KNX wired test - control a basic DPT1 binary switch', function (t) { let counter = 0 - const connection = new Connection({ + const connection = new KnxClient({ debug: true, physAddr: options.physAddr, handlers: { diff --git a/test/wiredtests/test-logotimer.ts b/test/wiredtests/test-logotimer.ts index 727f7b8..069d73e 100644 --- a/test/wiredtests/test-logotimer.ts +++ b/test/wiredtests/test-logotimer.ts @@ -2,7 +2,7 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint } from '../../src' +import { KnxClient, Datapoint } from '../../src' import test from 'tape' import util from 'util' import options from './wiredtest-options' @@ -13,7 +13,7 @@ import options from './wiredtest-options' ========== ================== */ test('KNX wired test - control a DPT9 timer', function (t) { - const connection = new Connection({ + const connection = new KnxClient({ // debug: true, handlers: { connected: () => { diff --git a/test/wiredtests/test-read.ts b/test/wiredtests/test-read.ts index 83685e5..32e35e6 100644 --- a/test/wiredtests/test-read.ts +++ b/test/wiredtests/test-read.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint } from '../../src' +import { KnxClient, Datapoint } from '../../src' import test from 'tape' import util from 'util' import options from './wiredtest-options' @@ -14,7 +14,7 @@ import options from './wiredtest-options' ========== ================== */ test('KNX wired test - read a temperature', function (t) { - const connection = new Connection({ + const connection = new KnxClient({ debug: true, physAddr: options.physAddr, handlers: { diff --git a/test/wiredtests/test-readstorm.ts b/test/wiredtests/test-readstorm.ts index 3078eea..be6cd63 100644 --- a/test/wiredtests/test-readstorm.ts +++ b/test/wiredtests/test-readstorm.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { Connection, Datapoint } from '../../src' +import { KnxClient, Datapoint } from '../../src' import test from 'tape' import util from 'util' import options from './wiredtest-options.js' @@ -49,7 +49,7 @@ test('KNX wired test - read multiple statuses from a consecutive GA range', func setupDatapoint(ctrl_ga, stat_ga) } } - const connection = new Connection({ + const connection = new KnxClient({ loglevel: 'warn', // forceTunneling: true, // minimumDelay: 100, diff --git a/typescript-sample/test-toggle-onoff.ts b/typescript-sample/test-toggle-onoff.ts index 349b0c4..16ef59a 100644 --- a/typescript-sample/test-toggle-onoff.ts +++ b/typescript-sample/test-toggle-onoff.ts @@ -3,11 +3,11 @@ * (C) 2016-2017 Elias Karakoulakis */ -import { Connection, Datapoint } from 'knx' +import { KnxClient, Datapoint } from 'knx' var groupAddress = process.argv[2] -var connection = new Connection({ +var connection = new KnxClient({ ipAddr: process.env.KNXGW, handlers: { connected: onConnected From f1213db5a6764da949a34b2907810eb14d28d8d9 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 4 Apr 2024 10:28:45 +0200 Subject: [PATCH 21/36] docs: fix tests --- manualtest/test-toggle.ts | 7 +++---- manualtest/test-writestorm.ts | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/manualtest/test-toggle.ts b/manualtest/test-toggle.ts index c7e7e95..5422408 100644 --- a/manualtest/test-toggle.ts +++ b/manualtest/test-toggle.ts @@ -18,12 +18,12 @@ const connection = new KnxClient({ console.log('Connected!'); console.log('----------'); // define a datapoint: - var dp = new Datapoint({ + const dp = new Datapoint({ ga: process.argv[2], dpt: 'DPT1.001' }, connection); if (process.argv[3]) { - var status_ga = new Datapoint({ + const status_ga = new Datapoint({ ga: process.argv[3], dpt: 'DPT1.001' }, connection); @@ -36,14 +36,13 @@ const connection = new KnxClient({ console.log('\n\n\n'); console.log('PRESS ANY KEY TO TOGGLE %s AND "q" TO QUIT.', process.argv[2]); console.log('\n\n\n'); - var dpVal = false; + let dpVal = false; process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on('data', (data) => { console.log(JSON.stringify(data)); if (data[0] === 113) { process.exit(0); - return; } dpVal = !dpVal; console.log("Sending " + dpVal); diff --git a/manualtest/test-writestorm.ts b/manualtest/test-writestorm.ts index a9300a6..e5f6fac 100644 --- a/manualtest/test-writestorm.ts +++ b/manualtest/test-writestorm.ts @@ -10,21 +10,21 @@ if (process.argv.length < 2) { process.exit(1); } -function setupSwitch(groupaddress, statusga) { - var sw = new Devices.BinarySwitch({ga: groupaddress, status_ga: statusga}, connection); +function setupSwitch(groupaddress: string, statusga: string) { + const sw = new Devices.BinarySwitch({ga: groupaddress, status_ga: statusga}, connection); sw.on('change', (oldvalue: boolean, newvalue: boolean, ga: string) => { console.log(" %s: **** %s current value: %j", Date.now(), ga, newvalue); }); return sw; } -var connection = new KnxClient({ +const connection = new KnxClient({ //debug: true, //minimumDelay: 10, handlers: { connected: function() { console.log('===========\nConnected! %s \n===========', Date.now()); - var v = parseInt(process.argv[2]); + const v = parseInt(process.argv[2]); console.log('---- Writing %d ---', v); setupSwitch('1/1/0', '1/1/100').write(v); setupSwitch('1/1/1', '1/1/101').write(v); From be1badcbd458f3a12bbd5e2cd11713da2ee9a982 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 4 Apr 2024 10:29:28 +0200 Subject: [PATCH 22/36] fix: import --- manualtest/test-toggle.ts | 2 +- manualtest/test-writestorm.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manualtest/test-toggle.ts b/manualtest/test-toggle.ts index 5422408..d980f5c 100644 --- a/manualtest/test-toggle.ts +++ b/manualtest/test-toggle.ts @@ -3,7 +3,7 @@ * (C) 2016-2017 Elias Karakoulakis */ -import { Datapoint, KnxClient } from "src"; +import { Datapoint, KnxClient } from "../src"; if (process.argv.length < 3) { console.log('usage: %s to toggle a light on & off', diff --git a/manualtest/test-writestorm.ts b/manualtest/test-writestorm.ts index e5f6fac..1394092 100644 --- a/manualtest/test-writestorm.ts +++ b/manualtest/test-writestorm.ts @@ -3,7 +3,7 @@ * (C) 2016-2017 Elias Karakoulakis */ Error.stackTraceLimit = Infinity; -import { Devices, KnxClient } from "src"; +import { Devices, KnxClient } from "../src"; if (process.argv.length < 2) { console.log('usage: %s <0/1> (off/on) to write to a set of binary switches', process.argv[1]); From 3949a2a6a5c9cd67a29784fe7e9a0ccbb1c0c810 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 4 Apr 2024 11:54:06 +0200 Subject: [PATCH 23/36] fix: use relative imports --- src/Datapoint.ts | 2 +- src/dptlib/dpt18.ts | 2 +- src/dptlib/dpt2.ts | 2 +- src/dptlib/dpt237.ts | 2 +- src/dptlib/dpt3.ts | 2 +- src/dptlib/index.ts | 4 ++-- test/dptlib/test-dpt10.ts | 2 +- test/dptlib/test-dpt11.ts | 2 +- test/dptlib/test-dpt19.ts | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Datapoint.ts b/src/Datapoint.ts index 745a208..a0662da 100644 --- a/src/Datapoint.ts +++ b/src/Datapoint.ts @@ -22,7 +22,7 @@ class Datapoint extends EventEmitter { private conn: any - constructor(options: Options, conn: any) { + constructor(options: Options, conn?: any) { if (options == null || options.ga == null) throw new Error('must supply at least { ga, dpt }!') super() diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts index b5dd0b7..74b8f85 100644 --- a/src/dptlib/dpt18.ts +++ b/src/dptlib/dpt18.ts @@ -5,7 +5,7 @@ import { logger } from 'log-driver' import type { DatapointConfig } from '.' -import { hasProp } from 'src/utils' +import { hasProp } from '../utils' // // DPT18: 8-bit Scene Control diff --git a/src/dptlib/dpt2.ts b/src/dptlib/dpt2.ts index ea9eabd..5b83d4b 100644 --- a/src/dptlib/dpt2.ts +++ b/src/dptlib/dpt2.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { hasProp } from 'src/utils' +import { hasProp } from '../utils' import type { DatapointConfig } from '.' import KnxLog from '../KnxLog' diff --git a/src/dptlib/dpt237.ts b/src/dptlib/dpt237.ts index 342e616..681a72d 100644 --- a/src/dptlib/dpt237.ts +++ b/src/dptlib/dpt237.ts @@ -5,7 +5,7 @@ import { logger } from 'log-driver' import type { DatapointConfig } from '.' -import { hasProp } from 'src/utils' +import { hasProp } from '../utils' const log = logger diff --git a/src/dptlib/dpt3.ts b/src/dptlib/dpt3.ts index 900eda6..4e43bd6 100644 --- a/src/dptlib/dpt3.ts +++ b/src/dptlib/dpt3.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { hasProp } from 'src/utils' +import { hasProp } from '../utils' import type { DatapointConfig } from '.' import KnxLog from '../KnxLog' diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts index 6254b90..788a819 100644 --- a/src/dptlib/index.ts +++ b/src/dptlib/index.ts @@ -62,8 +62,8 @@ import DPT21 from './dpt21' import DPT232 from './dpt232' import DPT237 from './dpt237' import DPT238 from './dpt238' -import { Datagram } from 'src/KnxClient' -import { hasProp } from 'src/utils' +import type { Datagram } from '../KnxClient' +import { hasProp } from '../utils' const log = KnxLog.get() diff --git a/test/dptlib/test-dpt10.ts b/test/dptlib/test-dpt10.ts index e137daf..24e408c 100644 --- a/test/dptlib/test-dpt10.ts +++ b/test/dptlib/test-dpt10.ts @@ -5,7 +5,7 @@ import test from 'tape' import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' -import { Datagram } from 'src/KnxClient' +import { Datagram } from '../../src/KnxClient' function timecompare(date1, sign, date2) { const dow1 = date1.getDay() diff --git a/test/dptlib/test-dpt11.ts b/test/dptlib/test-dpt11.ts index d0bf6c9..60246d0 100644 --- a/test/dptlib/test-dpt11.ts +++ b/test/dptlib/test-dpt11.ts @@ -5,7 +5,7 @@ import test from 'tape' import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' -import { Datagram } from 'src/KnxClient' +import { Datagram } from '../../src/KnxClient' function dateequals(d1: Date, d2: Date) { const d = d1.getDate() diff --git a/test/dptlib/test-dpt19.ts b/test/dptlib/test-dpt19.ts index ed9b1e4..ae621ab 100644 --- a/test/dptlib/test-dpt19.ts +++ b/test/dptlib/test-dpt19.ts @@ -4,8 +4,8 @@ */ import test from 'tape' -import DPTLib, { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' -import { Datagram } from 'src/KnxClient' +import { fromBuffer, populateAPDU, resolve } from '../../src/dptlib' +import { Datagram } from '../../src/KnxClient' test('DPT19 datetime conversion', function (t) { const tests = ['1995-12-17T03:24:00', '1996-07-17T03:24:00'] From 1e843eeeab6778a0343381f361e40b41d9f748c0 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 4 Apr 2024 12:02:15 +0200 Subject: [PATCH 24/36] chore: remove manualtest from ts build --- tsconfig.build.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index 7c9b76b..160c3e4 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,5 +1,5 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "build"], + "exclude": ["node_modules", "test", "build", "manualtest"], } \ No newline at end of file From 5466dbae76194b6fcfaf76697e2c4f6cf26664af Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 4 Apr 2024 15:03:10 +0200 Subject: [PATCH 25/36] fix: better types for Connect and BindSocket --- src/IpRoutingConnection.ts | 10 +++++----- src/IpTunnelingConnection.ts | 6 ++---- src/KnxClient.ts | 31 +++++++++++++------------------ 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index 9ea2096..1d740e5 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -6,9 +6,7 @@ import type { KnxClient } from './KnxClient' function IpRoutingConnection(instance: KnxClient): KnxClient { const log = KnxLog.get() - instance.BindSocket = function BindSocket( - cb: (socket: dgram.Socket) => void, - ) { + instance.BindSocket = function (cb) { const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true }) udpSocket.on('listening', () => { log.debug( @@ -32,14 +30,16 @@ function IpRoutingConnection(instance: KnxClient): KnxClient { } }) // ROUTING multicast connections need to bind to the default port, 3671 - udpSocket.bind(3671, () => cb && cb(udpSocket)) + udpSocket.bind(3671, () => { + if (cb) cb(udpSocket) + }) return udpSocket } // /// Start the connection /// - instance.Connect = function Connect() { + instance.Connect = function () { this.localAddress = this.getLocalAddress() this.socket = this.BindSocket((socket: dgram.Socket) => { socket.on('error', (errmsg: string) => diff --git a/src/IpTunnelingConnection.ts b/src/IpTunnelingConnection.ts index 28aa1f2..96ae25c 100644 --- a/src/IpTunnelingConnection.ts +++ b/src/IpTunnelingConnection.ts @@ -5,9 +5,7 @@ import type { KnxClient } from './KnxClient' function IpTunnelingConnection(instance: KnxClient) { const log = KnxLog.get() - instance.BindSocket = function BindSocket( - cb: (socket: dgram.Socket) => void, - ) { + instance.BindSocket = function (cb) { const udpSocket = dgram.createSocket('udp4') udpSocket.bind(() => { log.debug( @@ -20,7 +18,7 @@ function IpTunnelingConnection(instance: KnxClient) { return udpSocket } - instance.Connect = function Connect() { + instance.Connect = function () { this.localAddress = this.getLocalAddress() // create the socket this.socket = this.BindSocket((socket: dgram.Socket) => { diff --git a/src/KnxClient.ts b/src/KnxClient.ts index 0e7b8f5..b85d6eb 100644 --- a/src/KnxClient.ts +++ b/src/KnxClient.ts @@ -128,12 +128,6 @@ export class KnxClient extends KnxFSM { protected useTunneling: boolean - protected remoteEndpoint: { - addrstring: string - addr: any - port: number - } - protected localEchoInTunneling: boolean | undefined protected channel_id?: any @@ -158,14 +152,24 @@ export class KnxClient extends KnxFSM { protected writer: Writer - protected socket: Socket - protected usingMulticastTunneling: boolean protected minimumDelay: number + public remoteEndpoint: { + addrstring: string + addr: any + port: number + } + + public socket: Socket + public localAddress: string | null + public BindSocket: (this: this, cb: (socket: Socket) => void) => Socket + + public Connect: (this: this) => void + constructor(options: KnxOptions) { super() @@ -308,14 +312,6 @@ export class KnxClient extends KnxFSM { return candidateInterfaces } - BindSocket(cb: (socket: any) => void) { - // THIS IS A STUB and should be overridden by the connection type - } - - Connect() { - // THIS IS A STUB and should be overridden by the connection type - } - /** * -------------------------------- * Connection management methods @@ -327,9 +323,8 @@ export class KnxClient extends KnxFSM { // get the incoming packet's service type ... try { const reader = KnxNetProtocol.createReader(msg) - // TODO: improve types for binary protocol reader.KNXNetHeader('tmp') - const dg = reader.next()['tmp'] + const dg = reader.next()['tmp'] as Datagram const descr = datagramDesc(dg) KnxLog.get().trace( '(%s): Received %s message: %j', From de9a06ab53717dcec8a19f558f905ced5a6735ed Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 14:12:09 +0200 Subject: [PATCH 26/36] feat: add some missing dpts --- package.json | 2 +- src/dptlib/dpt10.ts | 49 +++--- src/dptlib/dpt18.ts | 16 +- src/dptlib/dpt19.ts | 3 +- src/dptlib/dpt2.ts | 2 +- src/dptlib/dpt20.ts | 16 +- src/dptlib/dpt21.ts | 3 - src/dptlib/dpt213.ts | 147 ++++++++++++++++++ src/dptlib/dpt22.ts | 169 ++++++++++++++++++++ src/dptlib/dpt235.ts | 97 ++++++++++++ src/dptlib/dpt242.ts | 112 ++++++++++++++ src/dptlib/dpt249.ts | 109 +++++++++++++ src/dptlib/dpt251.ts | 111 +++++++++++++ src/dptlib/dpt275.ts | 81 ++++++++++ src/dptlib/dpt28.ts | 66 ++++++++ src/dptlib/dpt29.ts | 61 ++++++++ src/dptlib/dpt3.ts | 2 +- src/dptlib/dpt4.ts | 2 +- src/dptlib/dpt60001.ts | 342 +++++++++++++++++++++++++++++++++++++++++ src/dptlib/dpt9.ts | 24 +-- src/dptlib/dpt999.ts | 62 ++++++++ src/utils.ts | 38 +++++ 22 files changed, 1442 insertions(+), 72 deletions(-) create mode 100644 src/dptlib/dpt213.ts create mode 100644 src/dptlib/dpt22.ts create mode 100644 src/dptlib/dpt235.ts create mode 100644 src/dptlib/dpt242.ts create mode 100644 src/dptlib/dpt249.ts create mode 100644 src/dptlib/dpt251.ts create mode 100644 src/dptlib/dpt275.ts create mode 100644 src/dptlib/dpt28.ts create mode 100644 src/dptlib/dpt29.ts create mode 100644 src/dptlib/dpt60001.ts create mode 100644 src/dptlib/dpt999.ts diff --git a/package.json b/package.json index a4a1e4c..66bea87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "knx", - "description": "KNXnet/IP protocol implementation for Node(>=6.x)", + "description": "KNXnet/IP protocol implementation", "version": "2.5.4", "engines": { "node": ">=16" diff --git a/src/dptlib/dpt10.ts b/src/dptlib/dpt10.ts index 557717a..75fdab7 100644 --- a/src/dptlib/dpt10.ts +++ b/src/dptlib/dpt10.ts @@ -65,33 +65,42 @@ const config: DatapointConfig = { // return a JS Date from a DPT10 payload, with DOW/hour/month/seconds set to the buffer values. // The week/month/year are inherited from the current timestamp. fromBuffer: (buf) => { - if (buf.length !== 3) - return log.warn('DPT10: Buffer should be 3 bytes long') - const [dnh, minutes, seconds] = buf - const dow = (dnh & 0b11100000) >> 5 - const hours = dnh & 0b00011111 + if (buf.length !== 3) { + log.error('DPT10: Buffer should be 3 bytes long, got', buf.length) + return null + } + + const d = new Date() + let dow = (buf[0] & 0b11100000) >> 5 // Day of week + const hours = buf[0] & 0b00011111 + const minutes = buf[1] + const seconds = buf[2] if ( - hours < 0 || - hours > 23 || - minutes < 0 || - minutes > 59 || - seconds < 0 || - seconds > 59 - ) - return log.warn( + hours >= 0 && + hours <= 23 && + minutes >= 0 && + minutes <= 59 && + seconds >= 0 && + seconds <= 59 + ) { + // 18/10/2021 if dow = 0, then the KNX device has not sent this optional value. + if (d.getDay() !== dow && dow > 0) { + if (dow === 7) dow = 0 // 18/10/2021 fix for the Sunday + // adjust day of month to get the day of week right + d.setDate(d.getDate() + dow - d.getDay()) + } + d.setHours(hours) + d.setMinutes(minutes) + d.setSeconds(seconds) + } else { + log.warn( 'DPT10: buffer %j (decoded as %d:%d:%d) is not a valid time', buf, hours, minutes, seconds, ) - - const d = new Date() - if (d.getDay() !== dow) - // adjust day of month to get the day of week right - d.setDate(d.getDate() + dow - d.getDay()) - // TODO: Shouldn't this be UTCHours? - d.setHours(hours, minutes, seconds) + } return d }, diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts index 74b8f85..c29c0b2 100644 --- a/src/dptlib/dpt18.ts +++ b/src/dptlib/dpt18.ts @@ -25,14 +25,12 @@ import { hasProp } from '../utils' end */ -// TODO: implement fromBuffer, formatAPDU - const log = logger const config: DatapointConfig = { id: 'DPT18', formatAPDU(value) { - if (value == null) log.warn('DPT18: cannot write null value') + if (!value) log.warn('DPT18: cannot write null value') else { const apdu_data = Buffer.alloc(1) if ( @@ -47,7 +45,6 @@ const config: DatapointConfig = { const sVal = `${ value.save_recall }0${sSceneNumberbinary.padStart(6, '0')}` - // console.log("BANANA SEND HEX " + sVal.toString("hex").toUpperCase()) apdu_data[0] = parseInt(sVal, 2) // 0b10111111; } else { log.error( @@ -59,14 +56,12 @@ const config: DatapointConfig = { }, fromBuffer(buf) { - // console.log("BANANA BUFF RECEIVE HEX " + buf.toString("hex").toUpperCase()) if (buf.length !== 1) { log.error('DP18: Buffer should be 1 byte long') } else { const sBit = parseInt(buf.toString('hex').toUpperCase(), 16) .toString(2) .padStart(8, '0') // Get bit from hex - // console.log("BANANA BUFF RECEIVE BIT " + sBit) return { save_recall: sBit.substring(0, 1), scenenumber: parseInt(sBit.substring(2), 2) + 1, @@ -91,12 +86,3 @@ const config: DatapointConfig = { } export default config - -/* -02/April/2020 Supergiovane -USE: -Input must be an object: {save_recall, scenenumber} -save_recall: 0 = recall scene, 1 = save scene -scenenumber: the scene number, example 1 -Example: {save_recall=0, scenenumber=2} -*/ diff --git a/src/dptlib/dpt19.ts b/src/dptlib/dpt19.ts index 54d706e..2b4c1b5 100644 --- a/src/dptlib/dpt19.ts +++ b/src/dptlib/dpt19.ts @@ -7,7 +7,6 @@ import { logger } from 'log-driver' import type { DatapointConfig } from '.' const log = logger -// TODO: implement fromBuffer, formatAPDU // // DPT19: 8-byte Date and Time @@ -16,7 +15,7 @@ const log = logger const config: DatapointConfig = { id: 'DPT19', formatAPDU: (value) => { - if (!(value instanceof Date)) + if (typeof value !== 'object' || value.constructor.name !== 'Date') return log.error('DPT19: Must supply a Date object') // Sunday is 0 in Javascript, but 7 in KNX. diff --git a/src/dptlib/dpt2.ts b/src/dptlib/dpt2.ts index 5b83d4b..61d073e 100644 --- a/src/dptlib/dpt2.ts +++ b/src/dptlib/dpt2.ts @@ -21,7 +21,7 @@ const config: DatapointConfig = { // DPT2 frame description. // Always 8-bit aligned. formatAPDU: (value: Dpt2Value) => { - if (value == null) return log.error('DPT2: cannot write null value') + if (!value) return log.error('DPT2: cannot write null value') if ( typeof value === 'object' && diff --git a/src/dptlib/dpt20.ts b/src/dptlib/dpt20.ts index ef217d7..cdde1fd 100644 --- a/src/dptlib/dpt20.ts +++ b/src/dptlib/dpt20.ts @@ -16,20 +16,26 @@ const log = logger const config: DatapointConfig = { id: 'DPT20', formatAPDU: (value) => { - log.debug(`./knx/src/dpt20.js : input value = ${value}`) - return Buffer.from([value]) + const apdu_data = Buffer.alloc(1) + apdu_data[0] = value + log.debug( + `./knx/src/dpt20.js : input value = ${value} apdu_data = ${apdu_data}`, + ) + return apdu_data }, fromBuffer: (buf) => { - if (buf.length !== 1) throw Error('Buffer should be 1 bytes long') + if (buf.length !== 1) { + log.warn('DPT20: Buffer should be 1 byte long, got', buf.length) + return null + } const ret = buf.readUInt8(0) - log.debug(` dpt20.js fromBuffer : ${ret}`) return ret }, basetype: { bitlength: 8, - range: [undefined, undefined], // TODO: verify + range: [undefined, undefined], valuetype: 'basic', desc: '1-byte', }, diff --git a/src/dptlib/dpt21.ts b/src/dptlib/dpt21.ts index a81d903..4232bf0 100644 --- a/src/dptlib/dpt21.ts +++ b/src/dptlib/dpt21.ts @@ -17,7 +17,6 @@ const log = logger // - AlarmUnAck b3 // - reseverd b4-7 -// FIXME: help needed const config: DatapointConfig = { id: 'DPT21', formatAPDU(value) { @@ -56,14 +55,12 @@ const config: DatapointConfig = { } // return ret; }, - basetype: { bitlength: 8, range: [undefined, undefined], valuetype: 'composite', desc: '1-byte', }, - subtypes: { // 21.001 status - 5 bits '001': { diff --git a/src/dptlib/dpt213.ts b/src/dptlib/dpt213.ts new file mode 100644 index 0000000..f6ed7d1 --- /dev/null +++ b/src/dptlib/dpt213.ts @@ -0,0 +1,147 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { frexp, hasProp, ldexp } from '../utils' + +const log = logger + +// +// DPT213: Data Type 4x 16-Signed Value +// + +function getHex(_value: number) { + try { + const arr = frexp(_value) + const mantissa = arr[0] + const exponent = arr[1] + // find the minimum exponent that will upsize the normalized mantissa (0,5 to 1 range) + // in order to fit in 11 bits ([-2048, 2047]) + let max_mantissa = 0 + let e: number + for (e = exponent; e >= -15; e--) { + max_mantissa = ldexp(100 * mantissa, e) + if (max_mantissa > -2048 && max_mantissa < 2047) break + } + const sign = mantissa < 0 ? 1 : 0 + const mant = mantissa < 0 ? ~(max_mantissa ^ 2047) : max_mantissa + const exp = exponent - e + return [(sign << 7) + (exp << 3) + (mant >> 8), mant % 256] + } catch (error) { + // noop + } +} + +function getFloat(_value0: number, _value1: number) { + const sign = _value0 >> 7 + const exponent = (_value0 & 0b01111000) >> 3 + let mantissa = 256 * (_value0 & 0b00000111) + _value1 + mantissa = sign === 1 ? ~(mantissa ^ 2047) : mantissa + return parseFloat(ldexp(0.01 * mantissa, exponent).toPrecision(15)) +} + +// 07/01/2021 Supergiovane +// Send to BUS +const config: DatapointConfig = { + id: 'DPT213', + formatAPDU(value) { + const apdu_data = Buffer.alloc(8) // 4 x 2 bytes + + if ( + typeof value === 'object' && + hasProp(value, 'Comfort') && + value.Comfort >= -272 && + value.Comfort <= 655.34 && + hasProp(value, 'Standby') && + value.Standby >= -272 && + value.Standby <= 655.34 && + hasProp(value, 'Economy') && + value.Economy >= -272 && + value.Economy <= 655.34 && + hasProp(value, 'BuildingProtection') && + value.BuildingProtection >= -272 && + value.BuildingProtection <= 655.34 + ) { + // Comfort + const ArrComfort = getHex(value.Comfort) + apdu_data[0] = ArrComfort[0] + apdu_data[1] = ArrComfort[1] + + // Standby + const ArrStandby = getHex(value.Standby) + apdu_data[2] = ArrStandby[0] + apdu_data[3] = ArrStandby[1] + + // Economy + const ArrEconomy = getHex(value.Economy) + apdu_data[4] = ArrEconomy[0] + apdu_data[5] = ArrEconomy[1] + + // BuildingProtection + const ArrBuildingProtection = getHex(value.BuildingProtection) + apdu_data[6] = ArrBuildingProtection[0] + apdu_data[7] = ArrBuildingProtection[1] + // console.log(apdu_data); + return apdu_data + } + log.error( + 'DPT213: Must supply a payload like, for example: {Comfort:21, Standby:20, Economy:14, BuildingProtection:8}', + ) + }, + + // RX from BUS + fromBuffer(buf) { + if (buf.length !== 8) { + log.warn( + 'DPT213.fromBuffer: buf should be 4x2 bytes long (got %d bytes)', + buf.length, + ) + return null + } + // Preparo per l'avvento di Gozer il gozeriano. + const nComfort = getFloat(buf[0], buf[1]) + const nStandby = getFloat(buf[2], buf[3]) + const nEconomy = getFloat(buf[4], buf[5]) + const nbProt = getFloat(buf[6], buf[7]) + return { + Comfort: nComfort, + Standby: nStandby, + Economy: nEconomy, + BuildingProtection: nbProt, + } + }, + + // DPT213 basetype info + basetype: { + bitlength: 4 * 16, + valuetype: 'basic', + desc: '4x 16-Bit Signed Value', + }, + + // DPT213 subtypes + subtypes: { + 100: { + desc: 'DPT_TempRoomSetpSet[4]', + name: 'Room temperature setpoint (Comfort, Standby, Economy, Building protection)', + unit: '°C', + range: [-272, 655.34], + }, + 101: { + desc: 'DPT_TempDHWSetpSet[4]', + name: 'Room temperature setpoint DHW (LegioProtect, Normal, Reduced, Off/FrostProtect)', + unit: '°C', + range: [-272, 655.34], + }, + 102: { + desc: 'DPT_TempRoomSetpSetShift[4]', + name: 'Room temperature setpoint shift (Comfort, Standby, Economy, Building protection)', + unit: '°C', + range: [-272, 655.34], + }, + }, +} + +export default config diff --git a/src/dptlib/dpt22.ts b/src/dptlib/dpt22.ts new file mode 100644 index 0000000..5d5d744 --- /dev/null +++ b/src/dptlib/dpt22.ts @@ -0,0 +1,169 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hasProp } from '../utils' + +const log = logger + +// +// DPT22: 2-byte RHCC status +// + +function reverseString(str: string) { + let newString = '' + for (let i = str.length - 1; i >= 0; i--) { + newString += str[i] + } + return newString +} + +interface DPT22Value { + Fault: boolean + StatusEcoH: boolean + TempFlowLimit: boolean + TempReturnLimit: boolean + StatusMorningBoostH: boolean + StatusStartOptim: boolean + StatusStopOptim: boolean + HeatingDisabled: boolean + HeatCoolMode: boolean + StatusEcoC: boolean + StatusPreCool: boolean + CoolingDisabled: boolean + DewPointStatus: boolean + FrostAlarm: boolean + OverheatAlarm: boolean + reserved: boolean +} + +const config: DatapointConfig = { + id: 'DPT22', + formatAPDU: (value: DPT22Value) => { + // Send to BUS + const apdu_data = Buffer.alloc(2) + if (!value) { + log.error('DPT232: cannot write null value') + } else { + if (typeof value === 'object') { + if (!hasProp(value, 'Fault')) value.Fault = false + if (!hasProp(value, 'StatusEcoH')) value.StatusEcoH = false + if (!hasProp(value, 'TempFlowLimit')) + value.TempFlowLimit = false + if (!hasProp(value, 'TempReturnLimit')) + value.TempReturnLimit = false + if (!hasProp(value, 'StatusMorningBoostH')) + value.StatusMorningBoostH = false + if (!hasProp(value, 'StatusStartOptim')) + value.StatusStartOptim = false + if (!hasProp(value, 'StatusStopOptim')) + value.StatusStopOptim = false + if (!hasProp(value, 'HeatingDisabled')) + value.HeatingDisabled = false + if (!hasProp(value, 'HeatCoolMode')) value.HeatCoolMode = false + if (!hasProp(value, 'StatusEcoC')) value.StatusEcoC = false + if (!hasProp(value, 'StatusPreCool')) + value.StatusPreCool = false + if (!hasProp(value, 'CoolingDisabled')) + value.CoolingDisabled = false + if (!hasProp(value, 'DewPointStatus')) + value.DewPointStatus = false + if (!hasProp(value, 'FrostAlarm')) value.FrostAlarm = false + if (!hasProp(value, 'OverheatAlarm')) + value.OverheatAlarm = false + if (!hasProp(value, 'reserved')) value.reserved = true + } else { + log.error('DPT22: Must supply a correct payload. See wiki.') + } + let firstHex = '' + let secondHex = '' + firstHex = firstHex.concat( + ...[ + value.Fault, + value.StatusEcoH, + value.TempFlowLimit, + value.TempReturnLimit, + value.StatusMorningBoostH, + value.StatusStartOptim, + value.StatusStopOptim, + value.HeatingDisabled, + ].map((v) => { + return Number(v).toString() + }), + ) + secondHex = secondHex.concat( + ...[ + value.HeatCoolMode, + value.StatusEcoC, + value.StatusPreCool, + value.CoolingDisabled, + value.DewPointStatus, + value.FrostAlarm, + value.OverheatAlarm, + value.reserved, + ].map((v) => { + return Number(v).toString() + }), + ) + apdu_data[0] = parseInt(reverseString(secondHex), 2) + apdu_data[1] = parseInt(reverseString(firstHex), 2) + return apdu_data + } + }, + fromBuffer: (buf) => { + // RX from BUS + if (buf.length !== 2) { + log.warn('DPT22: Buffer should be 2 bytes long, got', buf.length) + return null + } + const byte1 = reverseString(buf[1].toString(2).padStart(8, '0')).split( + '', + ) + const byte2 = reverseString(buf[0].toString(2).padStart(8, '0')).split( + '', + ) + const value: DPT22Value = { + // byte1 + Fault: byte1[0] === '1', + StatusEcoH: byte1[1] === '1', + TempFlowLimit: byte1[2] === '1', + TempReturnLimit: byte1[3] === '1', + StatusMorningBoostH: byte1[4] === '1', + StatusStartOptim: byte1[5] === '1', + StatusStopOptim: byte1[6] === '1', + HeatingDisabled: byte1[7] === '1', + // byte2 + HeatCoolMode: byte2[0] === '1', + StatusEcoC: byte2[1] === '1', + StatusPreCool: byte2[2] === '1', + CoolingDisabled: byte2[3] === '1', + DewPointStatus: byte2[4] === '1', + FrostAlarm: byte2[5] === '1', + OverheatAlarm: byte2[6] === '1', + reserved: byte2[7] === '1', + } + + return value + }, + basetype: { + bitlength: 16, + range: [undefined, undefined], + valuetype: 'basic', + desc: '2-byte', + }, + subtypes: { + // 22.101 DPT_StatusRHCC + 101: { + name: 'RHCC Status', + desc: 'RHCC Status', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +} + +export default config diff --git a/src/dptlib/dpt235.ts b/src/dptlib/dpt235.ts new file mode 100644 index 0000000..e564df4 --- /dev/null +++ b/src/dptlib/dpt235.ts @@ -0,0 +1,97 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hasProp, hex2bin } from '../utils' + +const log = logger +// +// DPT235: DPT_Tariff_ActiveEnergy +// + +// 08/09/2020 Supergiovane +// Send to BUS +const config: DatapointConfig = { + id: 'DPT235', + formatAPDU(value) { + try { + const apdu_data = Buffer.alloc(6) // 3 x 2 bytes + + if ( + typeof value === 'object' && + hasProp(value, 'activeElectricalEnergy') && + hasProp(value, 'tariff') && + hasProp(value, 'validityTariff') && + hasProp(value, 'validityEnergy') + ) { + // activeElectricalEnergy + const nbuff = Buffer.alloc(4) + nbuff.writeInt32BE(value.activeElectricalEnergy) + apdu_data[0] = nbuff[0] + apdu_data[1] = nbuff[1] + apdu_data[2] = nbuff[2] + apdu_data[3] = nbuff[3] + + // tariff + const tariff = parseInt(value.tariff) + apdu_data[4] = tariff + + // Validity + const validity = parseInt( + `000000${ + value.validityTariff ? '1' : '0' + }${value.validityEnergy ? '1' : '0'}`, + 2, + ) + apdu_data[5] = validity + return apdu_data + } + log.error( + 'DPT235: Must supply a payload like, for example: {activeElectricalEnergy:1540, tariff:20, validityTariff:true, validityEnergy:true}', + ) + } catch (error) { + log.error(`DPT235: exports.formatAPDU error ${error.message}`) + } + }, + + // RX from BUS + fromBuffer(buf) { + try { + // Preparo per l'avvento di Gozer il gozeriano. + const activeElectricalEnergy = buf.subarray(0, 4).readInt32BE() // First 4x8 bits signed integer + const tariff = parseInt(buf.subarray(4, 5)[0] as any) // next 8 bit unsigned value + const validity = hex2bin(buf.subarray(5, 6)[0].toString(16)) // Next 8 bit, only the latest 2 bits are used. + const validityTariff = validity.substring(6, 7) === '1' + const validityEnergy = validity.substring(7, 8) === '1' + return { + activeElectricalEnergy, + tariff, + validityTariff, + validityEnergy, + } + } catch (error) { + log.error(`DPT235: exports.fromBuffer error ${error.message}`) + } + }, + + // DPT basetype info + basetype: { + bitlength: 48, + valuetype: 'basic', + desc: '6 octect Tariff_ActiveEnergy', + }, + + // DPT subtypes + subtypes: { + '001': { + desc: 'DPT_Tariff_ActiveEnergy', + name: 'Tariff of active Energy (Energy+Tariff+Validity)', + unit: 'Tariff', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt242.ts b/src/dptlib/dpt242.ts new file mode 100644 index 0000000..e8f95bc --- /dev/null +++ b/src/dptlib/dpt242.ts @@ -0,0 +1,112 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hasProp, hex2bin } from '../utils' + +const log = logger + +interface DPT242Value { + x: number + y: number + brightness: number + isColorValid: boolean + isBrightnessValid: boolean +} + +// +// DPT242: 3-byte RGB xyY +// +const config: DatapointConfig = { + id: 'DPT242', + formatAPDU(value) { + if (!value) { + log.error('DPT242: cannot write null value') + } else { + if ( + typeof value === 'object' && + hasProp(value, 'isColorValid') && + hasProp(value, 'isBrightnessValid') && + hasProp(value, 'x') && + value.x >= 0 && + value.x <= 65535 && + hasProp(value, 'y') && + value.y >= 0 && + value.y <= 65535 && + hasProp(value, 'brightness') && + value.brightness >= 0 && + value.brightness <= 100 + ) { + // noop + } else { + log.error( + 'DPT242: Must supply an value {x:0-65535, y:0-65535, brightness:0-100, isColorValid:true/false, isBrightnessValid:true/false}', + ) + } + + const bufferTotal = Buffer.alloc(6) + const bufX = Buffer.alloc(2) + const bufY = Buffer.alloc(2) + const bufBrightness = Buffer.alloc(2) + const isColorValid = value.isColorValid ? '1' : '0' + const isBrightnessValid = value.isBrightnessValid ? '1' : '0' + bufX.writeUInt16BE(value.x) // buf.writeUInt16LE(number); + bufY.writeUInt16BE(value.y) + bufBrightness.writeUInt16BE(value.brightness) + bufBrightness[0] = parseInt( + `000000${isColorValid}${isBrightnessValid}`, + 2, + ) // .toString(16) as any // these are Colour and Brighness validities + bufferTotal[0] = bufX[0] + bufferTotal[1] = bufX[1] + bufferTotal[2] = bufY[0] + bufferTotal[3] = bufY[1] + bufferTotal[4] = bufBrightness[1] + bufferTotal[5] = bufBrightness[0] + + return bufferTotal + } + }, + + fromBuffer(buf) { + if (buf.length !== 6) { + log.error('DPT242: Buffer should be 6 bytes long, got', buf.length) + return null + } + const bufTotale = buf.toString('hex') + // console.log("bufTotale STRINGA: " + bufTotale); + const x = bufTotale.substring(0, 4) + const y = bufTotale.substring(4, 8) + const brightness = bufTotale.substring(8, 10) // these are Colour and Brighness validities (2 bit) //00000011 + const CB = bufTotale.substring(10, 12) + const isColorValid = hex2bin(CB).substring(6, 7) === '1' + const isBrightnessValid = hex2bin(CB).substring(7, 8) === '1' + const ret: DPT242Value = { + x: parseInt(x, 16), + y: parseInt(y, 16), + brightness: parseInt(brightness, 16), + isColorValid, + isBrightnessValid, + } + return ret + }, + basetype: { + bitlength: 3 * 16, + valuetype: 'basic', + desc: 'RGB xyY', + }, + subtypes: { + 600: { + desc: 'RGB xyY', + name: 'RGB color xyY', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +} + +export default config diff --git a/src/dptlib/dpt249.ts b/src/dptlib/dpt249.ts new file mode 100644 index 0000000..a647bbf --- /dev/null +++ b/src/dptlib/dpt249.ts @@ -0,0 +1,109 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hasProp, hex2bin } from '../utils' + +const log = logger + +// +// DPT249: 3-byte RGB xyY +// The info about validity of Colour and Brighness are omitted. +// +const config: DatapointConfig = { + id: 'DPT249', + formatAPDU(value) { + if (!value) { + log.error('DPT249: cannot write null value') + } else { + if ( + typeof value === 'object' && + hasProp(value, 'isTimePeriodValid') && + hasProp(value, 'isAbsoluteColourTemperatureValid') && + hasProp(value, 'isAbsoluteBrightnessValid') && + hasProp(value, 'transitionTime') && + hasProp(value, 'colourTemperature') && + value.colourTemperature >= 0 && + value.colourTemperature <= 65535 && + hasProp(value, 'absoluteBrightness') && + value.absoluteBrightness >= 0 && + value.absoluteBrightness <= 100 + ) { + // noop + } else { + log.error( + 'DPT249: Must supply an value, for example {transitionTime:100, colourTemperature:1000, absoluteBrightness:80, isTimePeriodValid:true, isAbsoluteColourTemperatureValid:true, isAbsoluteBrightnessValid:true}', + ) + } + + const bufferTotal = Buffer.alloc(6) + const transitionTime = Buffer.alloc(2) + const colourTemperature = Buffer.alloc(2) + const absoluteBrightness = Buffer.alloc(2) + const isTimePeriodValid = value.isTimePeriodValid ? '1' : '0' + const isAbsoluteColourTemperatureValid = + value.isAbsoluteColourTemperatureValid ? '1' : '0' + const isAbsoluteBrightnessValid = value.isAbsoluteBrightnessValid + ? '1' + : '0' + transitionTime.writeUInt16BE(value.transitionTime) // buf.writeUInt16LE(number); + colourTemperature.writeUInt16BE(value.colourTemperature) + absoluteBrightness.writeUInt16BE(value.absoluteBrightness) + absoluteBrightness[0] = parseInt( + `00000${isTimePeriodValid}${isAbsoluteColourTemperatureValid}${isAbsoluteBrightnessValid}`, + 2, + ) // .toString(16) // these are Colour and Brighness validities + bufferTotal[0] = transitionTime[0] + bufferTotal[1] = transitionTime[1] + bufferTotal[2] = colourTemperature[0] + bufferTotal[3] = colourTemperature[1] + bufferTotal[4] = absoluteBrightness[1] + bufferTotal[5] = absoluteBrightness[0] + return bufferTotal + } + }, + fromBuffer(buf) { + if (buf.length !== 6) { + log.error('DPT249: Buffer should be 6 bytes long, got', buf.length) + return null + } + const bufTotale = buf.toString('hex') + const transitionTime = bufTotale.substring(0, 4) + const colourTemperature = bufTotale.substring(4, 8) + const absoluteBrightness = bufTotale.substring(8, 10) // This is 1 Byte of validities (3 bit) //00000111 + const CB = bufTotale.substring(10, 12) + const isTimePeriodValid = hex2bin(CB).substring(5, 6) === '1' + const isAbsoluteColourTemperatureValid = + hex2bin(CB).substring(6, 7) === '1' + const isAbsoluteBrightnessValid = hex2bin(CB).substring(7, 8) === '1' + const ret = { + transitionTime: parseInt(transitionTime, 16), + colourTemperature: parseInt(colourTemperature, 16), + absoluteBrightness: parseInt(absoluteBrightness, 16), + isTimePeriodValid, + isAbsoluteColourTemperatureValid, + isAbsoluteBrightnessValid, + } + return ret + }, + basetype: { + bitlength: 2 * 16 + 2 * 8, + valuetype: 'basic', + desc: 'PDT_GENERIC_06', + }, + + subtypes: { + 600: { + desc: 'DPT_Brightness_Colour_Temperature_Transition', + name: 'Brightness Colour Temperature Transition', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +} + +export default config diff --git a/src/dptlib/dpt251.ts b/src/dptlib/dpt251.ts new file mode 100644 index 0000000..1ca9ee4 --- /dev/null +++ b/src/dptlib/dpt251.ts @@ -0,0 +1,111 @@ +/** + +* (C) 2020 Supergiovane +*/ + +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hasProp, hex2bin } from '../utils' + +const log = logger + +// Structure of DPT 251.600 +// Byte 0: R value +// Byte 1: G value +// Byte 2: B value +// Byte 3: W value +// Byte 4: 0x00 (reserved) +// Byte 5: +// Bit 0: W value valid? +// Bit 1: B value valid? +// Bit 2: G value valid? +// Bit 3: R value valid? +// Bit 4-7: 0 +// The bitfield which defines whether an value(R, G, B, W) is valid or not should be in the last byte of the array and not at the beginning.This can be verified by trying to send a DPT 251.600 telegram in the BUS monitor of the ETS5 application. + +const config: DatapointConfig = { + id: 'DPT251', + formatAPDU(value) { + if (!value) { + log.error('DPT251: cannot write null value') + } else { + if ( + typeof value === 'object' && + hasProp(value, 'white') && + value.white >= 0 && + value.white <= 255 && + hasProp(value, 'red') && + value.red >= 0 && + value.red <= 255 && + hasProp(value, 'green') && + value.green >= 0 && + value.green <= 255 && + hasProp(value, 'blue') && + value.blue >= 0 && + value.blue <= 255 && + hasProp(value, 'mR') && + hasProp(value, 'mG') && + hasProp(value, 'mB') && + hasProp(value, 'mW') + ) { + // noop + } else { + log.error( + 'DPT251: Must supply a value payload: {red:0-255, green:0-255, blue:0-255, white:0-255, mR:0-1, mG:0-1, mB:0-1, mW:0-1}', + ) + } + const bitVal = parseInt( + `0000${value.mR}${value.mG}${value.mB}${value.mW}`, + 2, + ) + + return Buffer.from([ + Math.floor(value.red), + Math.floor(value.green), + Math.floor(value.blue), + Math.floor(value.white), + Math.floor(0), + Math.floor(bitVal), + ]) + } + }, + fromBuffer(buf) { + if (buf.length !== 6) { + log.error('DPT251: Buffer should be 6 bytes long, got', buf.length) + return null + } + const valByte = buf[5].toString(2) // Get validity bits + const ret = { + red: buf[0], + green: buf[1], + blue: buf[2], + white: buf[3], + mR: parseInt(valByte[0]) || 0, + mG: parseInt(valByte[1]) || 0, + mB: parseInt(valByte[2]) || 0, + mW: parseInt(valByte[3]) || 0, + } + return ret + }, + basetype: { + bitlength: 6 * 8, + valuetype: 'basic', + desc: 'RGBW array', + }, + subtypes: { + 600: { + desc: 'RGBW [payload:{red:255, green:200, blue:30, white:50, mR:1, mG:1, mB:1, mW:1}]', + name: 'RGB color triplet + White + Validity', + unit: '', + scalar_range: [undefined, undefined], + range: [undefined, undefined], + }, + }, +} + +export default config diff --git a/src/dptlib/dpt275.ts b/src/dptlib/dpt275.ts new file mode 100644 index 0000000..29f0a02 --- /dev/null +++ b/src/dptlib/dpt275.ts @@ -0,0 +1,81 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hasProp } from '../utils' +import dpt9 from './dpt9' + +const log = logger + +// +// 4x DPT9.* 2-byte floating point value +// +const config: DatapointConfig = { + id: 'DPT275', + formatAPDU(value) { + // Get the javascript object and create a telegram for the KNX bus. + if ( + typeof value === 'object' && + hasProp(value, 'comfort') && + hasProp(value, 'standby') && + hasProp(value, 'economy') && + hasProp(value, 'buildingProtection') + ) { + const comfort = dpt9.formatAPDU(value.comfort) as Buffer + const standby = dpt9.formatAPDU(value.standby) as Buffer + const economy = dpt9.formatAPDU(value.economy) as Buffer + const buildingProtection = dpt9.formatAPDU( + value.buildingProtection, + ) as Buffer + return Buffer.concat([ + comfort, + standby, + economy, + buildingProtection, + ]) + } + log.error( + 'DPT275.formatAPDU: Must supply all values, for example {comfort:22, standby:21.5, economy:21, buildingProtection:15}', + ) + }, + fromBuffer(buf) { + // Get the telegram from the KNX bus and create a javascript object. + if (buf.length !== 8) { + log.warn( + 'DPT275.fromBuffer: buf should be 8 bytes long (got %d bytes)', + buf.length, + ) + return null + } + const comfort = dpt9.fromBuffer(buf.slice(0, 2)) + const standby = dpt9.fromBuffer(buf.slice(2, 4)) + const economy = dpt9.fromBuffer(buf.slice(4, 6)) + const buildingProtection = dpt9.fromBuffer(buf.slice(6, 8)) + return { + comfort, + standby, + economy, + buildingProtection, + } + }, + + // DPT275 basetype info + basetype: { + bitlength: 64, + valuetype: 'basic', + desc: 'Quadruple setpoints (comfort,standby,economy,buildingProtection) (4 float with 16 Bit)', + }, + + // DPT9 subtypes + subtypes: { + 100: { + name: 'Quadruple setpoints (comfort,standby,economy,buildingProtection) (4 float with 16 Bit)', + desc: 'DPT_TempRoomSetpSetF16[4]', + unit: '°C', + range: [-273, 670760], + }, + }, +} diff --git a/src/dptlib/dpt28.ts b/src/dptlib/dpt28.ts new file mode 100644 index 0000000..e29bfe7 --- /dev/null +++ b/src/dptlib/dpt28.ts @@ -0,0 +1,66 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' + +const log = logger + +// +// DPT28: ASCII string (variable length) UTF-8 +// + +// Write to BUS +const config: DatapointConfig = { + id: 'DPT28', + formatAPDU(value) { + if (typeof value !== 'string') { + log.error('Must supply a string value. Autoconversion to string') + try { + value = value.toString() + } catch (error) { + value = 'DPT Err' + } + } + + try { + const buf = Buffer.alloc(14) + if (this.subtypeid === '001') buf.write(value, 'utf-8') + return buf + } catch (error) { + return error.message + } + }, + + // Read from BUS + fromBuffer(buf) { + // nog length check because this is variable + // if (buf.length != 14) { + // knxLog.get().error('DPT28: Buffer should be 14 byte long, got', buf.length) + // return null + // } + if (this.subtypeid === '001') return buf.toString('utf-8') + }, + + // DPT28 basetype info + basetype: { + bitlength: 14 * 8, + valuetype: 'basic', + desc: '14-character string', + }, + + // DPT28 subtypes + subtypes: { + // 28.001 UTF-8 string + '001': { + use: 'G', + desc: 'DPT_String_UTF-8', + name: 'UTF-8 string', + force_encoding: 'UTF-8', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt29.ts b/src/dptlib/dpt29.ts new file mode 100644 index 0000000..d7f18cb --- /dev/null +++ b/src/dptlib/dpt29.ts @@ -0,0 +1,61 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { DatapointConfig } from '.' + +// +// DPT29: 8-byte signed value +// + +const config: DatapointConfig = { + id: 'DPT29', + formatAPDU(value) { + if (typeof value === 'string') value = BigInt(value) + const apdu_data = Buffer.allocUnsafe(8) + // Writing big integer value into buffer + // by using writeBigInt64BE() method + apdu_data.writeBigInt64BE(value, 0) + return apdu_data + }, + fromBuffer(buf) { + return buf.readBigInt64BE(0) + + // const bufInt = (buf.readUInt32BE(0) << 8) + buf.readUInt32BE(4) + // return bufInt.toString(16) + }, + basetype: { + bitlength: 64, + signedness: 'signed', + valuetype: 'basic', + desc: '8-byte V64 signed value', + // range: [-9223372036854775808, 9223372036854775807], + }, + + // DPT29 subtypes + subtypes: { + '010': { + use: 'G', + desc: 'DPT_ActiveEnergy_64', + name: 'Active energy V64 (Wh)', + unit: 'Wh', + }, + + '011': { + use: 'G', + desc: 'DPT_ApparantEnergy_V64', + name: 'Apparant energy V64 (VAh)', + unit: 'VAh', + }, + + '012': { + use: 'G', + desc: 'DPT_ReactiveEnergy_V64', + name: 'Reactive energy V64 (VARh)', + unit: 'VARh', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt3.ts b/src/dptlib/dpt3.ts index 4e43bd6..dde81d3 100644 --- a/src/dptlib/dpt3.ts +++ b/src/dptlib/dpt3.ts @@ -18,7 +18,7 @@ interface Dpt3Value { const config: DatapointConfig = { id: 'DPT3', formatAPDU: (value: Dpt3Value) => { - if (value == null) return log.warn('DPT3: cannot write null value') + if (!value) return log.warn('DPT3: cannot write null value') if ( typeof value === 'object' && diff --git a/src/dptlib/dpt4.ts b/src/dptlib/dpt4.ts index 1f71307..2625bcd 100644 --- a/src/dptlib/dpt4.ts +++ b/src/dptlib/dpt4.ts @@ -15,7 +15,7 @@ const log = KnxLog.get() const config: DatapointConfig = { id: 'DPT4', formatAPDU: (value: string): Buffer | void => { - if (value == null) return log.warn('DPT4: cannot write null value') + if (!value) return log.warn('DPT4: cannot write null value') if (typeof value !== 'string') return log.warn('DPT4: Must supply a character or string') diff --git a/src/dptlib/dpt60001.ts b/src/dptlib/dpt60001.ts new file mode 100644 index 0000000..365eecb --- /dev/null +++ b/src/dptlib/dpt60001.ts @@ -0,0 +1,342 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hasProp, hex2bin } from '../utils' + +const log = logger + +function toRadix(value: number, radix: number) { + if (!Number.isSafeInteger(value)) { + log.error('value must be a safe integer') + } + + const digits = Math.ceil(64 / Math.log2(radix)) + const twosComplement = + value < 0 ? BigInt(radix) ** BigInt(digits) + BigInt(value) : value + + return twosComplement.toString(radix).padStart(digits, '0') +} + +function griesserSectorCode(Byte0: number, Byte1: number) { + const SectorCode = Byte0 + (Byte1 & 3) * 256 + return SectorCode +} + +function griesserCommandCode(Byte1: number) { + const command = (Byte1 / 4) >> 0 + return command +} + +function griesserParameter(command, Byte2, Byte3, Byte4, Byte5) { + let commandOperation + switch (command) { + case 1: // drive command + switch (Byte2 & 31) { + case 0: + return 'no driving movement' + case 1: + return 'upper end position' + case 2: + return 'lower end position' + case 3: + if (Byte3 >= 1 && Byte3 <= 4) { + return `fixed position P${Byte3} approach` + } + return `Unknown value for Pn ${Byte3}` + + default: + return `Unknown drive command${Byte2}` + } + case 4: // set/delete lock + if (Byte2 === 0) { + return 'no lock' + } + switch (Byte2 & 3) { + case 1: + return 'driving command' + case 2: + return 'button lock' + case 3: + return 'driving command- and button lock' + } + if (Byte3 === 0) { + return 'delete lock' + } + return 'set lock' + + case 5: // operation code + if (Byte2 <= 6) { + commandOperation = 'groupoperation' + } else if (Byte2 >= 128 && Byte2 <= 134) { + commandOperation = 'localoperation' + } else { + commandOperation = `unknown command Byte3 for ${Byte2}` + } + if ((Byte2 & 127) === 0) { + return [commandOperation, 'long up'] + } + if ((Byte2 & 127) === 1) { + return [commandOperation, 'long down'] + } + if ((Byte2 & 127) === 2) { + return [commandOperation, 'short up'] + } + if ((Byte2 & 127) === 3) { + return [commandOperation, 'short down'] + } + if ((Byte2 & 127) === 4) { + return [commandOperation, 'stop'] + } + if ((Byte2 & 127) === 5) { + return [commandOperation, 'long-short up'] + } + if ((Byte2 & 127) === 6) { + return [commandOperation, 'long-short down'] + } + return [commandOperation, `unknown command Byte3 for ${Byte2}`] + + case 22: // driving range limits for automatic button commands + return [ + `min. angle: ${Byte2}`, + `max. angle: ${Byte3}`, + `min. height: ${Byte4}`, + `max. height: ${Byte5}`, + ] + default: + return `unknown value for command: ${command}` + } +} + +function griesserSectors(SectorCode) { + let SectorMin + let SectorMax + let dA + let a + let SectorCodeMin + let SectorCodeMax + dA = 1 + a = SectorCode + if (a > 0) { + while ((a & 1) === 0) { + a = (a / 2) >> 0 + dA *= 2 + } + dA -= 1 + SectorMin = SectorCode - dA + SectorMax = SectorCode + dA + } else { + SectorMin = 0 + SectorMax = 0 + } + if (SectorMin === 0) { + SectorCodeMin = 0 + } else { + SectorCodeMin = (((SectorMin - 1) / 2) >> 0) + 1 + } + if (SectorMax === 0) { + SectorCodeMax = 0 + } else { + SectorCodeMax = (((SectorMax - 1) / 2) >> 0) + 1 + } + if (SectorCodeMax === SectorCodeMin) { + return [SectorCodeMin] + } + const Sectors = [] + for (let i = SectorCodeMin; i <= SectorCodeMax; i++) { + Sectors.push(i) + } + return Sectors +} + +function griesserSectorToSectorCode(sectors) { + if (sectors.length === 1) { + return sectors[0] + sectors[0] - 1 + } + return Math.min(...sectors) + Math.max(...sectors) - 1 +} + +function griesserCommandToCommandCode(command) { + switch (command) { + case 'operation code': + return 5 + default: + log.error(`not implemented yet: ${command}`) + } +} + +function griesserCommandToCommandCodeP1(command) { + switch (command) { + case 'long up': + return 128 + case 'long down': + return 129 + case 'short up': + return 130 + case 'short down': + return 131 + case 'stop': + return 132 + case 'long-short up': + return 133 + case 'long-short down': + return 134 + default: + log.error(`unknown command: ${command}`) + } +} + +function griesserCommand(command) { + switch (command) { + case 1: + return 'drive command' + case 2: + return 'value correction' + case 3: + return 'automatic state' + case 4: + return 'set/delete lock' + case 5: + return 'operation code' + case 6: + return 'set scene' + case 7: + return 'special command' + case 8: + return 'date' + case 9: + return 'sync time' + case 10: + return 'sensor reading notification' + case 11: + return 'bus monitoring' + case 16: + return 'driving range limits for safety drive commands' + case 17: + return 'driving range limits for safety drive commands' + case 19: + return 'driving range limits for safety drive commands' + case 20: + return 'driving range limits for safety drive commands' + case 22: + return 'driving range limits for automatic drive commands' + case 23: + return 'driving range limits for automatic drive commands' + case 24: + return 'driving range limits for automatic drive commands' + default: + return `unknown value for function: ${command}` + } +} + +function griesserPrio(prio, command) { + const prioCommand = ((command & 224) / 32) >> 0 + if (((prio & 252) / 4) >> 0 === 0) { + switch (prioCommand) { + case 0: + return 'border command' + case 1: + return 'automatic command' + case 3: + return 'priority command' + case 4: + return 'warning command' + case 5: + return 'security command' + case 6: + return 'danger command' + default: + return `unknown priority${prioCommand}` + } + } else { + return '-' + } +} + +// Send to BUS +const config: DatapointConfig = { + id: 'DPT60001', + formatAPDU(value) { + if (!value) { + log.error('DPT60001: cannot write null value') + } else { + if ( + typeof value === 'object' && + Object.prototype.hasOwnProperty.call(value, 'command') && + Object.prototype.hasOwnProperty.call(value, 'data') && + Object.prototype.hasOwnProperty.call(value, 'sectors') && + value.data[0] === 'localoperation' + ) { + const sectorCode = griesserSectorToSectorCode(value.sectors) + const commandCode = griesserCommandToCommandCode(value.command) + const p1 = griesserCommandToCommandCodeP1(value.data[1]) + const bufferTotal = Buffer.alloc(6) + bufferTotal[0] = parseInt(toRadix(sectorCode, 2).slice(-8), 2) + bufferTotal[1] = parseInt( + toRadix(commandCode, 2).slice(-6) + + toRadix(sectorCode, 2).slice(-10, -8), + 2, + ) + bufferTotal[2] = parseInt(toRadix(p1, 2).slice(-8), 2) + return bufferTotal + } + log.error( + 'DPT60001: Must supply an value {command:"operation code", data:["localoperation", "long up"], sectors:[159]}', + ) + } + }, + + // RX from BUS + fromBuffer(buf) { + if (buf.length !== 6) { + log.warn( + 'DPTGriesser.fromBuffer: buf should be 6 bytes long (got %d bytes)', + buf.length, + ) + return null + } + const hexToDecimal = (hex) => parseInt(hex, 16) + const bufTotale = buf.toString('hex') + const Byte0 = hexToDecimal(bufTotale.slice(0, 2)) + const Byte1 = hexToDecimal(bufTotale.slice(2, 4)) + const Byte2 = hexToDecimal(bufTotale.slice(4, 6)) + const Byte3 = hexToDecimal(bufTotale.slice(6, 8)) + const Byte4 = hexToDecimal(bufTotale.slice(8, 10)) + const Byte5 = hexToDecimal(bufTotale.slice(10, 12)) + const sectorCode = griesserSectorCode(Byte0, Byte1) + const commandCode = griesserCommandCode(Byte1) + + return { + Byte0, + Byte1, + Byte2, + Byte3, + Byte4, + Byte5, + sectorCode, + commandCode, + sectors: griesserSectors(sectorCode), + prio: griesserPrio(Byte1, Byte2), + command: griesserCommand(commandCode), + data: griesserParameter(commandCode, Byte2, Byte3, Byte4, Byte5), + } + }, + + // DPT Griesser Object basetype info + basetype: { + bitlength: 4 * 8 + 2 * 6 + 1 * 10, + valuetype: 'composite', + desc: 'Commands for solar shading actors', + }, + subtypes: { + '001': { + desc: 'DPT_Griesser_Object', + name: 'Griesser Object', + }, + }, +} + +export default config diff --git a/src/dptlib/dpt9.ts b/src/dptlib/dpt9.ts index fe249ea..110b0d1 100644 --- a/src/dptlib/dpt9.ts +++ b/src/dptlib/dpt9.ts @@ -5,6 +5,7 @@ import { logger } from 'log-driver' import type { DatapointConfig } from '.' +import { frexp, ldexp } from '../utils' const log = logger @@ -12,29 +13,6 @@ const log = logger // DPT9.*: 2-byte floating point value // -// kudos to http://croquetweak.blogspot.gr/2014/08/deconstructing-floats-frexp-and-ldexp.html -const ldexp = (mantissa, exponent) => - // eslint-disable-next-line no-nested-ternary - exponent > 1023 // avoid multiplying by infinity - ? mantissa * 2 ** 1023 * 2 ** (exponent - 1023) - : exponent < -1074 // avoid multiplying by zero - ? mantissa * 2 ** -1074 * 2 ** (exponent + 1074) - : mantissa * 2 ** exponent - -const frexp = (value) => { - if (value === 0) return [0, 0] - const data = new DataView(new ArrayBuffer(8)) - data.setFloat64(0, value) - let bits = (data.getUint32(0) >>> 20) & 0x7ff - if (bits === 0) { - data.setFloat64(0, value * 2 ** 64) - bits = ((data.getUint32(0) >>> 20) & 0x7ff) - 64 - } - const exponent = bits - 1022 - const mantissa = ldexp(value, -exponent) - return [mantissa, exponent] -} - const config: DatapointConfig = { id: 'DPT9', formatAPDU: (value) => { diff --git a/src/dptlib/dpt999.ts b/src/dptlib/dpt999.ts new file mode 100644 index 0000000..28f67b9 --- /dev/null +++ b/src/dptlib/dpt999.ts @@ -0,0 +1,62 @@ +/** + * knx.js - a KNX protocol stack in pure Javascript + * (C) 2016-2018 Elias Karakoulakis + */ + +import { logger } from 'log-driver' +import type { DatapointConfig } from '.' +import { hexToDec } from '../utils' + +const log = logger + +// +// DPT999: 10 Bytes (RFID keypad style) +// +const config: DatapointConfig = { + id: 'DPT999', + formatAPDU(value) { + if (typeof value !== 'string' || value.length < 10) + log.warn( + "Must supply an HEX string value of 10 bytes. Please don't add '$' nor '0x' Example 12340000000000000000", + ) + else { + value = value + .toUpperCase() + .replace(/\$/g, '') + .replace(/0X/g, '') + .replace(/ /g, '') // Remove the $ and 0x + const apdu_data = Buffer.alloc(10) + let i = 0 + const iSlice = 2 + for (let index = 0; index < value.length; index += iSlice) { + const sHex = value.substring(index, iSlice + index) + const int = hexToDec(sHex) + apdu_data[i] = int + i++ + } + return apdu_data + } + }, + fromBuffer(buf) { + return buf.toString('hex') + }, + + // basetype info + basetype: { + bitlength: 80, + valuetype: 'basic', + desc: '10-bytes', + }, + + // DPT999 subtypes + subtypes: { + // 10 Bytes string hex value + '001': { + use: 'G', + desc: '10Bytes HEX', + name: '10 Bytes', + }, + }, +} + +export default config diff --git a/src/utils.ts b/src/utils.ts index 0b2b7da..b66e422 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,3 +2,41 @@ export function hasProp(obj: any, prop: string): boolean { return Object.prototype.hasOwnProperty.call(obj, prop) } + +export function hex2bin(hex: string) { + return parseInt(hex, 16).toString(2).padStart(8, '0') +} + +export function hexToDec(hex: string) { + let result = 0 + let digitValue: number + hex = hex.toLowerCase() + for (let i = 0; i < hex.length; i++) { + digitValue = '0123456789abcdefgh'.indexOf(hex[i]) + result = result * 16 + digitValue + } + return result +} + +export function ldexp(mantissa: number, exponent: number) { + // eslint-disable-next-line no-nested-ternary + return exponent > 1023 // avoid multiplying by infinity + ? mantissa * 2 ** 1023 * 2 ** (exponent - 1023) + : exponent < -1074 // avoid multiplying by zero + ? mantissa * 2 ** -1074 * 2 ** (exponent + 1074) + : mantissa * 2 ** exponent +} + +export function frexp(value: number) { + if (value === 0) return [value, 0] + const data = new DataView(new ArrayBuffer(8)) + data.setFloat64(0, value) + let bits = (data.getUint32(0) >>> 20) & 0x7ff + if (bits === 0) { + data.setFloat64(0, value * 2 ** 64) + bits = ((data.getUint32(0) >>> 20) & 0x7ff) - 64 + } + const exponent = bits - 1022 + const mantissa = ldexp(value, -exponent) + return [mantissa, exponent] +} From 5eaad9c0ea87fb5e9ffb34fb4c811fb2feb8d8a6 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 15:32:19 +0200 Subject: [PATCH 27/36] fix: use correct logger in dpts --- src/dptlib/dpt1.ts | 6 ++--- src/dptlib/dpt10.ts | 17 ++++++++------ src/dptlib/dpt11.ts | 13 ++++++----- src/dptlib/dpt14.ts | 9 ++++---- src/dptlib/dpt16.ts | 6 ++--- src/dptlib/dpt18.ts | 10 ++++----- src/dptlib/dpt19.ts | 8 +++---- src/dptlib/dpt2.ts | 14 ++++++------ src/dptlib/dpt20.ts | 11 ++++----- src/dptlib/dpt21.ts | 16 ++++++------- src/dptlib/dpt213.ts | 8 +++---- src/dptlib/dpt22.ts | 16 +++++++------ src/dptlib/dpt232.ts | 9 ++++---- src/dptlib/dpt235.ts | 9 ++++---- src/dptlib/dpt237.ts | 14 ++++++------ src/dptlib/dpt238.ts | 6 ++--- src/dptlib/dpt242.ts | 13 ++++++----- src/dptlib/dpt249.ts | 13 ++++++----- src/dptlib/dpt251.ts | 15 +++++++------ src/dptlib/dpt275.ts | 8 +++---- src/dptlib/dpt28.ts | 10 ++++----- src/dptlib/dpt3.ts | 10 ++++----- src/dptlib/dpt4.ts | 12 +++++----- src/dptlib/dpt60001.ts | 51 ++++++++++++++++++++++-------------------- src/dptlib/dpt9.ts | 10 ++++----- src/dptlib/dpt999.ts | 6 ++--- src/dptlib/index.ts | 3 --- 27 files changed, 155 insertions(+), 168 deletions(-) diff --git a/src/dptlib/dpt1.ts b/src/dptlib/dpt1.ts index 42c516a..34a3462 100644 --- a/src/dptlib/dpt1.ts +++ b/src/dptlib/dpt1.ts @@ -3,11 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - const custom_truthiness = (value: string | boolean): boolean => { const f = parseFloat(value as string) return !isNaN(f) && isFinite(f) @@ -23,7 +21,7 @@ const config: DatapointConfig = { fromBuffer: (buf) => { if (buf.length !== 1) { - log.warn( + Log.get().warn( 'DPT1.fromBuffer: buf should be 1 byte (got %d bytes)', buf.length, ) diff --git a/src/dptlib/dpt10.ts b/src/dptlib/dpt10.ts index 75fdab7..2903e6d 100644 --- a/src/dptlib/dpt10.ts +++ b/src/dptlib/dpt10.ts @@ -4,11 +4,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT10.*: time (3 bytes) // @@ -40,13 +38,15 @@ const config: DatapointConfig = { minute = parseInt(match[4]) second = parseInt(match[5]) } else { - log.warn('DPT10: invalid time format (%s)', value) + Log.get().warn('DPT10: invalid time format (%s)', value) } } break case 'object': if (value.constructor.name !== 'Date') { - log.warn('Must supply a Date or String for DPT10 time') + Log.get().warn( + 'Must supply a Date or String for DPT10 time', + ) break } case 'number': @@ -66,7 +66,10 @@ const config: DatapointConfig = { // The week/month/year are inherited from the current timestamp. fromBuffer: (buf) => { if (buf.length !== 3) { - log.error('DPT10: Buffer should be 3 bytes long, got', buf.length) + Log.get().error( + 'DPT10: Buffer should be 3 bytes long, got', + buf.length, + ) return null } @@ -93,7 +96,7 @@ const config: DatapointConfig = { d.setMinutes(minutes) d.setSeconds(seconds) } else { - log.warn( + Log.get().warn( 'DPT10: buffer %j (decoded as %d:%d:%d) is not a valid time', buf, hours, diff --git a/src/dptlib/dpt11.ts b/src/dptlib/dpt11.ts index bf477e8..4b95793 100644 --- a/src/dptlib/dpt11.ts +++ b/src/dptlib/dpt11.ts @@ -3,17 +3,17 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger // // DPT11.*: date // const config: DatapointConfig = { id: 'DPT11', formatAPDU: (value) => { - if (value == null) return log.error('cannot write null value for DPT11') + if (value == null) + return Log.get().error('cannot write null value for DPT11') switch (typeof value) { case 'string': case 'number': @@ -27,7 +27,7 @@ const config: DatapointConfig = { } } if (isNaN(value.getDate())) - return log.error( + return Log.get().error( 'Must supply a numeric timestamp, Date or String object for DPT11 Date', ) @@ -40,7 +40,8 @@ const config: DatapointConfig = { }, fromBuffer: (buf) => { - if (buf.length !== 3) return log.error('Buffer should be 3 bytes long') + if (buf.length !== 3) + return Log.get().error('Buffer should be 3 bytes long') const day = buf[0] & 31 // 0b00011111); const month = buf[1] & 15 // 0b00001111); let year = buf[2] & 127 // 0b01111111); @@ -53,7 +54,7 @@ const config: DatapointConfig = { year < 1990 || year > 2089 ) { - log.error( + Log.get().error( '%j => %d/%d/%d is not valid date according to DPT11, setting to 1990/01/01', buf, day, diff --git a/src/dptlib/dpt14.ts b/src/dptlib/dpt14.ts index 0ab739b..6cfd7a6 100644 --- a/src/dptlib/dpt14.ts +++ b/src/dptlib/dpt14.ts @@ -2,11 +2,9 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT14.*: 4-byte floating point value // @@ -18,14 +16,15 @@ const config: DatapointConfig = { id: 'DPT14', formatAPDU: (value) => { if (value == null || typeof value !== 'number') - log.error('DPT14: Must supply a number value') + Log.get().error('DPT14: Must supply a number value') const apdu_data = Buffer.alloc(4) apdu_data.writeFloatBE(value, 0) return apdu_data }, fromBuffer: (buf) => { - if (buf.length !== 4) log.warn('DPT14: Buffer should be 4 bytes long') + if (buf.length !== 4) + Log.get().warn('DPT14: Buffer should be 4 bytes long') return buf.readFloatBE(0) }, diff --git a/src/dptlib/dpt16.ts b/src/dptlib/dpt16.ts index 09e0396..52ef12b 100644 --- a/src/dptlib/dpt16.ts +++ b/src/dptlib/dpt16.ts @@ -3,11 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT16: ASCII string (max 14 chars) // @@ -16,7 +14,7 @@ const config: DatapointConfig = { id: 'DPT16', formatAPDU: (value) => { if (typeof value !== 'string') - return log.warn('Must supply a string value') + return Log.get().warn('Must supply a string value') const buf = Buffer.alloc(14) buf.write(value, 'ascii') diff --git a/src/dptlib/dpt18.ts b/src/dptlib/dpt18.ts index c29c0b2..b9f7994 100644 --- a/src/dptlib/dpt18.ts +++ b/src/dptlib/dpt18.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hasProp } from '../utils' @@ -25,12 +25,10 @@ import { hasProp } from '../utils' end */ -const log = logger - const config: DatapointConfig = { id: 'DPT18', formatAPDU(value) { - if (!value) log.warn('DPT18: cannot write null value') + if (!value) Log.get().warn('DPT18: cannot write null value') else { const apdu_data = Buffer.alloc(1) if ( @@ -47,7 +45,7 @@ const config: DatapointConfig = { }0${sSceneNumberbinary.padStart(6, '0')}` apdu_data[0] = parseInt(sVal, 2) // 0b10111111; } else { - log.error( + Log.get().error( 'DPT18: Must supply a value object of {save_recall, scenenumber}', ) } @@ -57,7 +55,7 @@ const config: DatapointConfig = { fromBuffer(buf) { if (buf.length !== 1) { - log.error('DP18: Buffer should be 1 byte long') + Log.get().error('DP18: Buffer should be 1 byte long') } else { const sBit = parseInt(buf.toString('hex').toUpperCase(), 16) .toString(2) diff --git a/src/dptlib/dpt19.ts b/src/dptlib/dpt19.ts index 2b4c1b5..a0346da 100644 --- a/src/dptlib/dpt19.ts +++ b/src/dptlib/dpt19.ts @@ -3,11 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT19: 8-byte Date and Time // @@ -16,7 +14,7 @@ const config: DatapointConfig = { id: 'DPT19', formatAPDU: (value) => { if (typeof value !== 'object' || value.constructor.name !== 'Date') - return log.error('DPT19: Must supply a Date object') + return Log.get().error('DPT19: Must supply a Date object') // Sunday is 0 in Javascript, but 7 in KNX. const day = value.getDay() === 0 ? 7 : value.getDay() @@ -34,7 +32,7 @@ const config: DatapointConfig = { fromBuffer: (buf) => { if (buf.length !== 8) - return log.warn('DPT19: Buffer should be 8 bytes long') + return Log.get().warn('DPT19: Buffer should be 8 bytes long') return new Date( buf[0] + 1900, buf[1] - 1, diff --git a/src/dptlib/dpt2.ts b/src/dptlib/dpt2.ts index 61d073e..7c52087 100644 --- a/src/dptlib/dpt2.ts +++ b/src/dptlib/dpt2.ts @@ -5,10 +5,7 @@ import { hasProp } from '../utils' import type { DatapointConfig } from '.' - -import KnxLog from '../KnxLog' - -const log = KnxLog.get() +import Log from '../KnxLog' interface Dpt2Value { priority: boolean | number @@ -21,7 +18,7 @@ const config: DatapointConfig = { // DPT2 frame description. // Always 8-bit aligned. formatAPDU: (value: Dpt2Value) => { - if (!value) return log.error('DPT2: cannot write null value') + if (!value) return Log.get().error('DPT2: cannot write null value') if ( typeof value === 'object' && @@ -33,13 +30,16 @@ const config: DatapointConfig = { (value.data as number & 0b00000001), ]) - log.error('DPT2: Must supply an value {priority:, data:}') + Log.get().error( + 'DPT2: Must supply an value {priority:, data:}', + ) // FIXME: should this return zero buffer when error? Or nothing? return Buffer.from([0]) }, fromBuffer: (buf) => { - if (buf.length !== 1) return log.error('Buffer should be 1 byte long') + if (buf.length !== 1) + return Log.get().error('Buffer should be 1 byte long') return { priority: (buf[0] & 0b00000010) >> 1, diff --git a/src/dptlib/dpt20.ts b/src/dptlib/dpt20.ts index cdde1fd..1c964ce 100644 --- a/src/dptlib/dpt20.ts +++ b/src/dptlib/dpt20.ts @@ -3,11 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT20: 1-byte HVAC // @@ -18,7 +16,7 @@ const config: DatapointConfig = { formatAPDU: (value) => { const apdu_data = Buffer.alloc(1) apdu_data[0] = value - log.debug( + Log.get().debug( `./knx/src/dpt20.js : input value = ${value} apdu_data = ${apdu_data}`, ) return apdu_data @@ -26,7 +24,10 @@ const config: DatapointConfig = { fromBuffer: (buf) => { if (buf.length !== 1) { - log.warn('DPT20: Buffer should be 1 byte long, got', buf.length) + Log.get().warn( + 'DPT20: Buffer should be 1 byte long, got', + buf.length, + ) return null } const ret = buf.readUInt8(0) diff --git a/src/dptlib/dpt21.ts b/src/dptlib/dpt21.ts index 4232bf0..68d56eb 100644 --- a/src/dptlib/dpt21.ts +++ b/src/dptlib/dpt21.ts @@ -2,11 +2,9 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT21: 1-byte status // @@ -20,8 +18,9 @@ const log = logger const config: DatapointConfig = { id: 'DPT21', formatAPDU(value) { - if (value == null) return log.error('DPT21: cannot write null value') - log.debug(`./knx/src/dpt21.js : input value = ${value}`) + if (value == null) + return Log.get().error('DPT21: cannot write null value') + Log.get().debug(`./knx/src/dpt21.js : input value = ${value}`) // var apdu_data = Buffer.alloc(1); // apdu_data[0] = value; @@ -34,15 +33,16 @@ const config: DatapointConfig = { (value.alarmeunack << 4), ]) - log.error('DPT21: Must supply a value which is an object') + Log.get().error('DPT21: Must supply a value which is an object') // return apdu_data; return Buffer.from([0]) }, fromBuffer(buf) { - if (buf.length !== 1) return log.error('Buffer should be 1 bytes long') + if (buf.length !== 1) + return Log.get().error('Buffer should be 1 bytes long') // if (buf.length != 1) throw "Buffer should be 1 bytes long"; - log.debug(` dpt21.js fromBuffer : ${buf}`) + Log.get().debug(` dpt21.js fromBuffer : ${buf}`) // var ret = buf.readUInt8(0); diff --git a/src/dptlib/dpt213.ts b/src/dptlib/dpt213.ts index f6ed7d1..440f025 100644 --- a/src/dptlib/dpt213.ts +++ b/src/dptlib/dpt213.ts @@ -3,12 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { frexp, hasProp, ldexp } from '../utils' -const log = logger - // // DPT213: Data Type 4x 16-Signed Value // @@ -87,7 +85,7 @@ const config: DatapointConfig = { // console.log(apdu_data); return apdu_data } - log.error( + Log.get().error( 'DPT213: Must supply a payload like, for example: {Comfort:21, Standby:20, Economy:14, BuildingProtection:8}', ) }, @@ -95,7 +93,7 @@ const config: DatapointConfig = { // RX from BUS fromBuffer(buf) { if (buf.length !== 8) { - log.warn( + Log.get().warn( 'DPT213.fromBuffer: buf should be 4x2 bytes long (got %d bytes)', buf.length, ) diff --git a/src/dptlib/dpt22.ts b/src/dptlib/dpt22.ts index 5d5d744..30f9676 100644 --- a/src/dptlib/dpt22.ts +++ b/src/dptlib/dpt22.ts @@ -2,13 +2,10 @@ * knx.js - a KNX protocol stack in pure Javascript * (C) 2016-2018 Elias Karakoulakis */ - -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hasProp } from '../utils' -const log = logger - // // DPT22: 2-byte RHCC status // @@ -46,7 +43,7 @@ const config: DatapointConfig = { // Send to BUS const apdu_data = Buffer.alloc(2) if (!value) { - log.error('DPT232: cannot write null value') + Log.get().error('DPT232: cannot write null value') } else { if (typeof value === 'object') { if (!hasProp(value, 'Fault')) value.Fault = false @@ -76,7 +73,9 @@ const config: DatapointConfig = { value.OverheatAlarm = false if (!hasProp(value, 'reserved')) value.reserved = true } else { - log.error('DPT22: Must supply a correct payload. See wiki.') + Log.get().error( + 'DPT22: Must supply a correct payload. See wiki.', + ) } let firstHex = '' let secondHex = '' @@ -116,7 +115,10 @@ const config: DatapointConfig = { fromBuffer: (buf) => { // RX from BUS if (buf.length !== 2) { - log.warn('DPT22: Buffer should be 2 bytes long, got', buf.length) + Log.get().warn( + 'DPT22: Buffer should be 2 bytes long, got', + buf.length, + ) return null } const byte1 = reverseString(buf[1].toString(2).padStart(8, '0')).split( diff --git a/src/dptlib/dpt232.ts b/src/dptlib/dpt232.ts index 8489fb1..1197f0e 100644 --- a/src/dptlib/dpt232.ts +++ b/src/dptlib/dpt232.ts @@ -3,11 +3,9 @@ * (C) 2016-2019 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT232: 3-byte RGB color array // MSB: Red, Green, LSB: Blue @@ -15,7 +13,8 @@ const log = logger const config: DatapointConfig = { id: 'DPT232', formatAPDU: (value) => { - if (value == null) return log.error('DPT232: cannot write null value') + if (value == null) + return Log.get().error('DPT232: cannot write null value') if (typeof value === 'object') { const { red, green, blue } = value @@ -29,7 +28,7 @@ const config: DatapointConfig = { ) return Buffer.from([red, green, blue]) } - log.error( + Log.get().error( 'DPT232: Must supply an value {red:0..255, green:0.255, blue:0.255}', ) }, diff --git a/src/dptlib/dpt235.ts b/src/dptlib/dpt235.ts index e564df4..79aa3c1 100644 --- a/src/dptlib/dpt235.ts +++ b/src/dptlib/dpt235.ts @@ -3,11 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hasProp, hex2bin } from '../utils' -const log = logger // // DPT235: DPT_Tariff_ActiveEnergy // @@ -49,11 +48,11 @@ const config: DatapointConfig = { apdu_data[5] = validity return apdu_data } - log.error( + Log.get().error( 'DPT235: Must supply a payload like, for example: {activeElectricalEnergy:1540, tariff:20, validityTariff:true, validityEnergy:true}', ) } catch (error) { - log.error(`DPT235: exports.formatAPDU error ${error.message}`) + Log.get().error(`DPT235: exports.formatAPDU error ${error.message}`) } }, @@ -73,7 +72,7 @@ const config: DatapointConfig = { validityEnergy, } } catch (error) { - log.error(`DPT235: exports.fromBuffer error ${error.message}`) + Log.get().error(`DPT235: exports.fromBuffer error ${error.message}`) } }, diff --git a/src/dptlib/dpt237.ts b/src/dptlib/dpt237.ts index 681a72d..d1c1720 100644 --- a/src/dptlib/dpt237.ts +++ b/src/dptlib/dpt237.ts @@ -3,21 +3,20 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hasProp } from '../utils' -const log = logger - // // DPT237: 2-byte unsigned value // const config: DatapointConfig = { id: 'DPT237', formatAPDU(value) { - if (value == null) return log.error('DPT237: cannot write null value') + if (value == null) + return Log.get().error('DPT237: cannot write null value') - log.trace(`dpt278.js : input value = ${value}`) + Log.get().trace(`dpt278.js : input value = ${value}`) const apdu_data = Buffer.alloc(2) @@ -44,7 +43,7 @@ const config: DatapointConfig = { return apdu_data } - log.error( + Log.get().error( 'DPT237: Must supply an value {address:[0,63] or [0,15], address type:{0,1}, ...}', ) @@ -52,7 +51,8 @@ const config: DatapointConfig = { }, fromBuffer(buf) { - if (buf.length !== 2) return log.error('Buffer should be 2 byte long') + if (buf.length !== 2) + return Log.get().error('Buffer should be 2 byte long') return { address: buf[1] & 0b00011111, diff --git a/src/dptlib/dpt238.ts b/src/dptlib/dpt238.ts index 464a931..d759a8d 100644 --- a/src/dptlib/dpt238.ts +++ b/src/dptlib/dpt238.ts @@ -3,11 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT238: 1-byte unsigned value // @@ -16,7 +14,7 @@ const config: DatapointConfig = { id: 'DPT238', formatAPDU: (value) => { const apdu_data = Buffer.from([value]) - log.trace( + Log.get().trace( `dpt238.js : input value = ${value} apdu_data = ${apdu_data}`, ) return apdu_data diff --git a/src/dptlib/dpt242.ts b/src/dptlib/dpt242.ts index e8f95bc..7386bc0 100644 --- a/src/dptlib/dpt242.ts +++ b/src/dptlib/dpt242.ts @@ -3,12 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hasProp, hex2bin } from '../utils' -const log = logger - interface DPT242Value { x: number y: number @@ -24,7 +22,7 @@ const config: DatapointConfig = { id: 'DPT242', formatAPDU(value) { if (!value) { - log.error('DPT242: cannot write null value') + Log.get().error('DPT242: cannot write null value') } else { if ( typeof value === 'object' && @@ -42,7 +40,7 @@ const config: DatapointConfig = { ) { // noop } else { - log.error( + Log.get().error( 'DPT242: Must supply an value {x:0-65535, y:0-65535, brightness:0-100, isColorValid:true/false, isBrightnessValid:true/false}', ) } @@ -73,7 +71,10 @@ const config: DatapointConfig = { fromBuffer(buf) { if (buf.length !== 6) { - log.error('DPT242: Buffer should be 6 bytes long, got', buf.length) + Log.get().error( + 'DPT242: Buffer should be 6 bytes long, got', + buf.length, + ) return null } const bufTotale = buf.toString('hex') diff --git a/src/dptlib/dpt249.ts b/src/dptlib/dpt249.ts index a647bbf..f349d74 100644 --- a/src/dptlib/dpt249.ts +++ b/src/dptlib/dpt249.ts @@ -3,12 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hasProp, hex2bin } from '../utils' -const log = logger - // // DPT249: 3-byte RGB xyY // The info about validity of Colour and Brighness are omitted. @@ -17,7 +15,7 @@ const config: DatapointConfig = { id: 'DPT249', formatAPDU(value) { if (!value) { - log.error('DPT249: cannot write null value') + Log.get().error('DPT249: cannot write null value') } else { if ( typeof value === 'object' && @@ -34,7 +32,7 @@ const config: DatapointConfig = { ) { // noop } else { - log.error( + Log.get().error( 'DPT249: Must supply an value, for example {transitionTime:100, colourTemperature:1000, absoluteBrightness:80, isTimePeriodValid:true, isAbsoluteColourTemperatureValid:true, isAbsoluteBrightnessValid:true}', ) } @@ -67,7 +65,10 @@ const config: DatapointConfig = { }, fromBuffer(buf) { if (buf.length !== 6) { - log.error('DPT249: Buffer should be 6 bytes long, got', buf.length) + Log.get().error( + 'DPT249: Buffer should be 6 bytes long, got', + buf.length, + ) return null } const bufTotale = buf.toString('hex') diff --git a/src/dptlib/dpt251.ts b/src/dptlib/dpt251.ts index 1ca9ee4..e5b32fd 100644 --- a/src/dptlib/dpt251.ts +++ b/src/dptlib/dpt251.ts @@ -8,11 +8,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -import { hasProp, hex2bin } from '../utils' - -const log = logger +import { hasProp } from '../utils' // Structure of DPT 251.600 // Byte 0: R value @@ -32,7 +30,7 @@ const config: DatapointConfig = { id: 'DPT251', formatAPDU(value) { if (!value) { - log.error('DPT251: cannot write null value') + Log.get().error('DPT251: cannot write null value') } else { if ( typeof value === 'object' && @@ -55,7 +53,7 @@ const config: DatapointConfig = { ) { // noop } else { - log.error( + Log.get().error( 'DPT251: Must supply a value payload: {red:0-255, green:0-255, blue:0-255, white:0-255, mR:0-1, mG:0-1, mB:0-1, mW:0-1}', ) } @@ -76,7 +74,10 @@ const config: DatapointConfig = { }, fromBuffer(buf) { if (buf.length !== 6) { - log.error('DPT251: Buffer should be 6 bytes long, got', buf.length) + Log.get().error( + 'DPT251: Buffer should be 6 bytes long, got', + buf.length, + ) return null } const valByte = buf[5].toString(2) // Get validity bits diff --git a/src/dptlib/dpt275.ts b/src/dptlib/dpt275.ts index 29f0a02..6386aa3 100644 --- a/src/dptlib/dpt275.ts +++ b/src/dptlib/dpt275.ts @@ -3,13 +3,11 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hasProp } from '../utils' import dpt9 from './dpt9' -const log = logger - // // 4x DPT9.* 2-byte floating point value // @@ -37,14 +35,14 @@ const config: DatapointConfig = { buildingProtection, ]) } - log.error( + Log.get().error( 'DPT275.formatAPDU: Must supply all values, for example {comfort:22, standby:21.5, economy:21, buildingProtection:15}', ) }, fromBuffer(buf) { // Get the telegram from the KNX bus and create a javascript object. if (buf.length !== 8) { - log.warn( + Log.get().warn( 'DPT275.fromBuffer: buf should be 8 bytes long (got %d bytes)', buf.length, ) diff --git a/src/dptlib/dpt28.ts b/src/dptlib/dpt28.ts index e29bfe7..5446934 100644 --- a/src/dptlib/dpt28.ts +++ b/src/dptlib/dpt28.ts @@ -3,11 +3,9 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -const log = logger - // // DPT28: ASCII string (variable length) UTF-8 // @@ -17,7 +15,9 @@ const config: DatapointConfig = { id: 'DPT28', formatAPDU(value) { if (typeof value !== 'string') { - log.error('Must supply a string value. Autoconversion to string') + Log.get().error( + 'Must supply a string value. Autoconversion to string', + ) try { value = value.toString() } catch (error) { @@ -38,7 +38,7 @@ const config: DatapointConfig = { fromBuffer(buf) { // nog length check because this is variable // if (buf.length != 14) { - // knxLog.get().error('DPT28: Buffer should be 14 byte long, got', buf.length) + // knxLog.get().get().error('DPT28: Buffer should be 14 byte long, got', buf.length) // return null // } if (this.subtypeid === '001') return buf.toString('utf-8') diff --git a/src/dptlib/dpt3.ts b/src/dptlib/dpt3.ts index dde81d3..5944e27 100644 --- a/src/dptlib/dpt3.ts +++ b/src/dptlib/dpt3.ts @@ -6,9 +6,7 @@ import { hasProp } from '../utils' import type { DatapointConfig } from '.' -import KnxLog from '../KnxLog' - -const log = KnxLog.get() +import Log from '../KnxLog' interface Dpt3Value { decr_incr: number @@ -18,7 +16,7 @@ interface Dpt3Value { const config: DatapointConfig = { id: 'DPT3', formatAPDU: (value: Dpt3Value) => { - if (!value) return log.warn('DPT3: cannot write null value') + if (!value) return Log.get().warn('DPT3: cannot write null value') if ( typeof value === 'object' && @@ -29,13 +27,13 @@ const config: DatapointConfig = { (value.decr_incr << 3) + (value.data & 0b00000111), ]) - log.error('Must supply a value object of {decr_incr, data}') + Log.get().error('Must supply a value object of {decr_incr, data}') // FIXME: should this return zero buffer when error? Or nothing? return Buffer.from([0]) }, fromBuffer: (buf: Buffer) => { if (buf.length !== 1) - return log.error('DPT3: Buffer should be 1 byte long') + return Log.get().error('DPT3: Buffer should be 1 byte long') return { decr_incr: (buf[0] & 0b00001000) >> 3, diff --git a/src/dptlib/dpt4.ts b/src/dptlib/dpt4.ts index 2625bcd..066dd51 100644 --- a/src/dptlib/dpt4.ts +++ b/src/dptlib/dpt4.ts @@ -4,10 +4,8 @@ */ import type { DatapointConfig } from '.' +import Log from '../KnxLog' -import KnxLog from '../KnxLog' - -const log = KnxLog.get() // // DPT4: 8-bit character // @@ -15,20 +13,20 @@ const log = KnxLog.get() const config: DatapointConfig = { id: 'DPT4', formatAPDU: (value: string): Buffer | void => { - if (!value) return log.warn('DPT4: cannot write null value') + if (!value) return Log.get().warn('DPT4: cannot write null value') if (typeof value !== 'string') - return log.warn('DPT4: Must supply a character or string') + return Log.get().warn('DPT4: Must supply a character or string') const apdu_data: number = value.charCodeAt(0) if (apdu_data > 255) - return log.warn('DPT4: must supply an ASCII character') + return Log.get().warn('DPT4: must supply an ASCII character') return Buffer.from([apdu_data]) }, fromBuffer: (buf: Buffer): string | void => { if (buf.length !== 1) - return log.warn('DPT4: Buffer should be 1 byte long') + return Log.get().warn('DPT4: Buffer should be 1 byte long') return String.fromCharCode(buf[0]) }, diff --git a/src/dptlib/dpt60001.ts b/src/dptlib/dpt60001.ts index 365eecb..e07ba80 100644 --- a/src/dptlib/dpt60001.ts +++ b/src/dptlib/dpt60001.ts @@ -3,15 +3,12 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' -import { hasProp, hex2bin } from '../utils' - -const log = logger function toRadix(value: number, radix: number) { if (!Number.isSafeInteger(value)) { - log.error('value must be a safe integer') + Log.get().error('value must be a safe integer') } const digits = Math.ceil(64 / Math.log2(radix)) @@ -31,8 +28,14 @@ function griesserCommandCode(Byte1: number) { return command } -function griesserParameter(command, Byte2, Byte3, Byte4, Byte5) { - let commandOperation +function griesserParameter( + command: number, + Byte2: number, + Byte3: number, + Byte4: number, + Byte5: number, +) { + let commandOperation: string switch (command) { case 1: // drive command switch (Byte2 & 31) { @@ -111,13 +114,13 @@ function griesserParameter(command, Byte2, Byte3, Byte4, Byte5) { } } -function griesserSectors(SectorCode) { - let SectorMin - let SectorMax - let dA - let a - let SectorCodeMin - let SectorCodeMax +function griesserSectors(SectorCode: number) { + let SectorMin: number + let SectorMax: number + let dA: number + let a: number + let SectorCodeMin: number + let SectorCodeMax: number dA = 1 a = SectorCode if (a > 0) { @@ -152,23 +155,23 @@ function griesserSectors(SectorCode) { return Sectors } -function griesserSectorToSectorCode(sectors) { +function griesserSectorToSectorCode(sectors: number[]) { if (sectors.length === 1) { return sectors[0] + sectors[0] - 1 } return Math.min(...sectors) + Math.max(...sectors) - 1 } -function griesserCommandToCommandCode(command) { +function griesserCommandToCommandCode(command: string) { switch (command) { case 'operation code': return 5 default: - log.error(`not implemented yet: ${command}`) + Log.get().error(`not implemented yet: ${command}`) } } -function griesserCommandToCommandCodeP1(command) { +function griesserCommandToCommandCodeP1(command: string) { switch (command) { case 'long up': return 128 @@ -185,11 +188,11 @@ function griesserCommandToCommandCodeP1(command) { case 'long-short down': return 134 default: - log.error(`unknown command: ${command}`) + Log.get().error(`unknown command: ${command}`) } } -function griesserCommand(command) { +function griesserCommand(command: number) { switch (command) { case 1: return 'drive command' @@ -232,7 +235,7 @@ function griesserCommand(command) { } } -function griesserPrio(prio, command) { +function griesserPrio(prio: number, command: number) { const prioCommand = ((command & 224) / 32) >> 0 if (((prio & 252) / 4) >> 0 === 0) { switch (prioCommand) { @@ -261,7 +264,7 @@ const config: DatapointConfig = { id: 'DPT60001', formatAPDU(value) { if (!value) { - log.error('DPT60001: cannot write null value') + Log.get().error('DPT60001: cannot write null value') } else { if ( typeof value === 'object' && @@ -283,7 +286,7 @@ const config: DatapointConfig = { bufferTotal[2] = parseInt(toRadix(p1, 2).slice(-8), 2) return bufferTotal } - log.error( + Log.get().error( 'DPT60001: Must supply an value {command:"operation code", data:["localoperation", "long up"], sectors:[159]}', ) } @@ -292,7 +295,7 @@ const config: DatapointConfig = { // RX from BUS fromBuffer(buf) { if (buf.length !== 6) { - log.warn( + Log.get().warn( 'DPTGriesser.fromBuffer: buf should be 6 bytes long (got %d bytes)', buf.length, ) diff --git a/src/dptlib/dpt9.ts b/src/dptlib/dpt9.ts index 110b0d1..46b08eb 100644 --- a/src/dptlib/dpt9.ts +++ b/src/dptlib/dpt9.ts @@ -3,12 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { frexp, ldexp } from '../utils' -const log = logger - // // DPT9.*: 2-byte floating point value // @@ -17,7 +15,9 @@ const config: DatapointConfig = { id: 'DPT9', formatAPDU: (value) => { if (!isFinite(value)) - return log.warn('DPT9: cannot write non-numeric or undefined value') + return Log.get().warn( + 'DPT9: cannot write non-numeric or undefined value', + ) const arr = frexp(value) const [mantissa, exponent] = arr @@ -38,7 +38,7 @@ const config: DatapointConfig = { fromBuffer: (buf) => { if (buf.length !== 2) - return log.warn( + return Log.get().warn( 'DPT9.fromBuffer: buf should be 2 bytes long (got %d bytes)', buf.length, ) diff --git a/src/dptlib/dpt999.ts b/src/dptlib/dpt999.ts index 28f67b9..1093eb6 100644 --- a/src/dptlib/dpt999.ts +++ b/src/dptlib/dpt999.ts @@ -3,12 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { logger } from 'log-driver' +import Log from '../KnxLog' import type { DatapointConfig } from '.' import { hexToDec } from '../utils' -const log = logger - // // DPT999: 10 Bytes (RFID keypad style) // @@ -16,7 +14,7 @@ const config: DatapointConfig = { id: 'DPT999', formatAPDU(value) { if (typeof value !== 'string' || value.length < 10) - log.warn( + Log.get().warn( "Must supply an HEX string value of 10 bytes. Please don't add '$' nor '0x' Example 12340000000000000000", ) else { diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts index 788a819..bb5698c 100644 --- a/src/dptlib/index.ts +++ b/src/dptlib/index.ts @@ -32,9 +32,6 @@ HVAC 1 Byte DPT 20 DPT 20 0.. Unlimited string 8859_1 . DPT 24 DPT 24 List 3-byte value 3 Byte DPT 232 DPT 232 RGB[0,0,0]...[255,255,255] */ - -import * as fs from 'fs' -import * as path from 'path' import * as util from 'util' import KnxLog from '../KnxLog' From 2829edff8eddc426b23d44b5b09c980536462198 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 15:33:59 +0200 Subject: [PATCH 28/36] fix: ensure logger is re-created when options are passed --- src/KnxLog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KnxLog.ts b/src/KnxLog.ts index 4e2dcd4..7160f84 100644 --- a/src/KnxLog.ts +++ b/src/KnxLog.ts @@ -31,7 +31,7 @@ const create = (options: KnxLogOptions): Logger => { const KnxLog: KnxLogger = { get: (options: KnxLogOptions): Logger => { - if (!logger) logger = create(options) + if (!logger || options) logger = create(options) return logger }, } From 8dae420d3f1ca9f335510a5315d3681714ac776c Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 15:38:31 +0200 Subject: [PATCH 29/36] fix: ensure logger is unique --- src/IpRoutingConnection.ts | 11 ++++------- src/IpTunnelingConnection.ts | 9 +++------ src/KnxClient.ts | 4 ++-- src/KnxLog.ts | 2 +- src/dptlib/index.ts | 6 ++---- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/IpRoutingConnection.ts b/src/IpRoutingConnection.ts index 1d740e5..de3d926 100644 --- a/src/IpRoutingConnection.ts +++ b/src/IpRoutingConnection.ts @@ -1,15 +1,12 @@ import * as util from 'util' import * as dgram from 'dgram' -import KnxLog from './KnxLog' import type { KnxClient } from './KnxClient' function IpRoutingConnection(instance: KnxClient): KnxClient { - const log = KnxLog.get() - instance.BindSocket = function (cb) { const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true }) udpSocket.on('listening', () => { - log.debug( + this.log.debug( util.format( 'IpRoutingConnection %s:%d, adding membership for %s', instance.localAddress, @@ -23,7 +20,7 @@ function IpRoutingConnection(instance: KnxClient): KnxClient { instance.localAddress, ) } catch (err) { - log.warn( + this.log.warn( 'IPRouting connection: cannot add membership (%s)', err, ) @@ -43,7 +40,7 @@ function IpRoutingConnection(instance: KnxClient): KnxClient { this.localAddress = this.getLocalAddress() this.socket = this.BindSocket((socket: dgram.Socket) => { socket.on('error', (errmsg: string) => - log.debug(util.format('Socket error: %j', errmsg)), + this.log.debug(util.format('Socket error: %j', errmsg)), ) socket.on( 'message', @@ -52,7 +49,7 @@ function IpRoutingConnection(instance: KnxClient): KnxClient { rinfo: dgram.RemoteInfo, callback: () => void, ) => { - log.debug( + this.log.debug( `Inbound multicast message from ${ rinfo.address }: ${msg.toString('hex')}`, diff --git a/src/IpTunnelingConnection.ts b/src/IpTunnelingConnection.ts index 96ae25c..95483e7 100644 --- a/src/IpTunnelingConnection.ts +++ b/src/IpTunnelingConnection.ts @@ -1,14 +1,11 @@ import dgram from 'dgram' -import KnxLog from './KnxLog' import type { KnxClient } from './KnxClient' function IpTunnelingConnection(instance: KnxClient) { - const log = KnxLog.get() - instance.BindSocket = function (cb) { const udpSocket = dgram.createSocket('udp4') udpSocket.bind(() => { - log.debug( + this.log.debug( 'IpTunnelingConnection.BindSocket %s:%d', instance.localAddress, udpSocket.address().port, @@ -23,7 +20,7 @@ function IpTunnelingConnection(instance: KnxClient) { // create the socket this.socket = this.BindSocket((socket: dgram.Socket) => { socket.on('error', (errmsg: string) => - log.debug('Socket error: %j', errmsg), + this.log.debug('Socket error: %j', errmsg), ) socket.on( 'message', @@ -32,7 +29,7 @@ function IpTunnelingConnection(instance: KnxClient) { rinfo: dgram.RemoteInfo, callback: () => void, ) => { - log.debug('Inbound message: %s', msg.toString('hex')) + this.log.debug('Inbound message: %s', msg.toString('hex')) this.onUdpSocketMessage(msg, rinfo, callback) }, ) diff --git a/src/KnxClient.ts b/src/KnxClient.ts index b85d6eb..efc78cc 100644 --- a/src/KnxClient.ts +++ b/src/KnxClient.ts @@ -118,8 +118,6 @@ export interface Datagram { export class KnxClient extends KnxFSM { protected options: KnxOptions - protected log: Logger - protected ThreeLevelGroupAddressing: boolean protected reconnection_cycles: number @@ -156,6 +154,8 @@ export class KnxClient extends KnxFSM { protected minimumDelay: number + public log: Logger + public remoteEndpoint: { addrstring: string addr: any diff --git a/src/KnxLog.ts b/src/KnxLog.ts index 7160f84..396300d 100644 --- a/src/KnxLog.ts +++ b/src/KnxLog.ts @@ -30,7 +30,7 @@ const create = (options: KnxLogOptions): Logger => { } const KnxLog: KnxLogger = { - get: (options: KnxLogOptions): Logger => { + get: (options?: KnxLogOptions): Logger => { if (!logger || options) logger = create(options) return logger }, diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts index bb5698c..fe25b5e 100644 --- a/src/dptlib/index.ts +++ b/src/dptlib/index.ts @@ -62,8 +62,6 @@ import DPT238 from './dpt238' import type { Datagram } from '../KnxClient' import { hasProp } from '../utils' -const log = KnxLog.get() - interface DatapointSubtype { scalar_range?: [number, number] name: string @@ -166,7 +164,7 @@ export function populateAPDU( if (hasProp(dpt, 'subtype') && hasProp(dpt.subtype, 'scalar_range')) { const [s_min, s_max] = dpt.subtype.scalar_range if (value < s_min || value > s_max) { - log.trace( + KnxLog.get().trace( 'Value %j(%s) out of scalar range(%j) for %s', value, typeof value, @@ -179,7 +177,7 @@ export function populateAPDU( tgtvalue = Math.round((value - b) / a) } } else if (value < r_min || value > r_max) { - log.trace( + KnxLog.get().trace( 'Value %j(%s) out of bounds(%j) for %s.%s', value, typeof value, From 49b200ec7d377de475a626350845830a1a55df61 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 15:40:45 +0200 Subject: [PATCH 30/36] fix: move info log in client constructor --- src/KnxClient.ts | 15 ++++++++++++++- src/index.ts | 16 ---------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/KnxClient.ts b/src/KnxClient.ts index efc78cc..7c81777 100644 --- a/src/KnxClient.ts +++ b/src/KnxClient.ts @@ -13,6 +13,10 @@ import { Logger, LogLevel } from 'log-driver' import { hasProp } from './utils' import KnxFSM from './FSM' +// do not use import here or package.json would be loaded twice +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkgJson = require('../package.json') + type KnxDeviceAddress = string type KnxGroupAddress = string @@ -173,8 +177,17 @@ export class KnxClient extends KnxFSM { constructor(options: KnxOptions) { super() - this.options = options || {} this.log = KnxLog.get(options) + this.log.info( + util.format( + 'Loading %s: %s, version: %s', + pkgJson.name, + pkgJson.description, + pkgJson.version, + ), + ) + + this.options = options || {} this.localAddress = null this.ThreeLevelGroupAddressing = true this.reconnection_cycles = 0 diff --git a/src/index.ts b/src/index.ts index b684418..496ea18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,26 +3,10 @@ * (C) 2016-2017 Elias Karakoulakis */ -import { format } from 'util' -import { logger } from 'log-driver' - import { KnxClient } from './KnxClient' import Datapoint from './Datapoint' import Devices from './devices' import Log from './KnxLog' import dpts from './dptlib' -// do not use import here or package.json would be loaded twice -// eslint-disable-next-line @typescript-eslint/no-var-requires -const pkgJson = require('../package.json') - -logger.info( - format( - 'Loading %s: %s, version: %s', - pkgJson.name, - pkgJson.description, - pkgJson.version, - ), -) - export { KnxClient, Datapoint, Devices, Log, dpts } From 3bbeb34a32ea218e258ba3fcc610f9d204b648fe Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 15:46:41 +0200 Subject: [PATCH 31/36] fix: load missing dpts --- src/dptlib/dpt275.ts | 2 ++ src/dptlib/index.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/dptlib/dpt275.ts b/src/dptlib/dpt275.ts index 6386aa3..c84fd7d 100644 --- a/src/dptlib/dpt275.ts +++ b/src/dptlib/dpt275.ts @@ -77,3 +77,5 @@ const config: DatapointConfig = { }, }, } + +export default config diff --git a/src/dptlib/index.ts b/src/dptlib/index.ts index fe25b5e..bf915f4 100644 --- a/src/dptlib/index.ts +++ b/src/dptlib/index.ts @@ -56,9 +56,21 @@ import DPT18 from './dpt18' import DPT19 from './dpt19' import DPT20 from './dpt20' import DPT21 from './dpt21' +import DPT22 from './dpt22' +import DPT28 from './dpt28' +import DPT29 from './dpt29' +import DPT213 from './dpt213' import DPT232 from './dpt232' +import DPT235 from './dpt235' import DPT237 from './dpt237' import DPT238 from './dpt238' +import DPT242 from './dpt242' +import DPT249 from './dpt249' +import DPT251 from './dpt251' +import DPT275 from './dpt275' +import DPT999 from './dpt999' +import DPT6001 from './dpt60001' + import type { Datagram } from '../KnxClient' import { hasProp } from '../utils' @@ -111,9 +123,20 @@ const dpts: Record = { [DPT19.id]: DPT19, [DPT20.id]: DPT20, [DPT21.id]: DPT21, + [DPT22.id]: DPT22, + [DPT28.id]: DPT28, + [DPT29.id]: DPT29, + [DPT213.id]: DPT213, [DPT232.id]: DPT232, + [DPT235.id]: DPT235, [DPT237.id]: DPT237, [DPT238.id]: DPT238, + [DPT242.id]: DPT242, + [DPT249.id]: DPT249, + [DPT251.id]: DPT251, + [DPT275.id]: DPT275, + [DPT999.id]: DPT999, + [DPT6001.id]: DPT6001, } export function resolve(dptid: string | number): DatapointConfig { From 2853126902d54d07f06e6d9f87258a14924c205b Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 15:47:10 +0200 Subject: [PATCH 32/36] fix: import type --- src/dptlib/dpt29.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dptlib/dpt29.ts b/src/dptlib/dpt29.ts index d7f18cb..d0de42f 100644 --- a/src/dptlib/dpt29.ts +++ b/src/dptlib/dpt29.ts @@ -3,7 +3,7 @@ * (C) 2016-2018 Elias Karakoulakis */ -import { DatapointConfig } from '.' +import type { DatapointConfig } from '.' // // DPT29: 8-byte signed value From 4d76d6b5fad5b4d1fe6b36a2aac31474d3d437fb Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 5 Apr 2024 15:49:36 +0200 Subject: [PATCH 33/36] fix: dpts tests --- test/dptlib/test-dpt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/dptlib/test-dpt.ts b/test/dptlib/test-dpt.ts index 28639ed..84e1d47 100644 --- a/test/dptlib/test-dpt.ts +++ b/test/dptlib/test-dpt.ts @@ -33,7 +33,7 @@ test('resolve', function (t) { t.throws( () => { - resolve('29.010') + resolve('1111111.010') }, /Unsupported DPT: .*/, 'Unsupported/unknown DPT', @@ -41,7 +41,7 @@ test('resolve', function (t) { t.throws( () => { - resolve(29) + resolve(1111111) }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT', @@ -49,7 +49,7 @@ test('resolve', function (t) { t.throws( () => { - resolve([29] as any) + resolve([1111111] as any) }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT', From 7f93ccc8bae3adc26d10013b14edf6fe16d0b8e7 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 11 Apr 2024 11:29:58 +0200 Subject: [PATCH 34/36] fix: better binary protocol types --- src/KnxProtocol.ts | 2 +- src/types/binary-protocol.d.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index e6f3b86..4860b11 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -52,7 +52,7 @@ KnxProtocol.define('IPv4Endpoint', { ) const [addr, port] = value.split(':') - this.raw(Buffer.from(ipaddr.parse(addr).toByteArray()), 4) + this.raw(Buffer.from(ipaddr.parse(addr).toByteArray())) this.UInt16BE(port) }, }) diff --git a/src/types/binary-protocol.d.ts b/src/types/binary-protocol.d.ts index d170d25..cd49d40 100644 --- a/src/types/binary-protocol.d.ts +++ b/src/types/binary-protocol.d.ts @@ -2,8 +2,8 @@ import { Duplex } from 'stream' declare module 'binary-protocol' { interface ProtocolConfig { - read(propertyName: string): void - write(value: any): void + read(this: Reader, propertyName: string): void + write(this: Writer, value: any): void } interface Reader { @@ -20,7 +20,7 @@ declare module 'binary-protocol' { end(fn: () => void): this finally(fn: () => void): this reset(): this - raw(property: string, fn: (value: any) => void): this + raw(property: string, length: number): this next(chunk?: any): any process(): any createLooper(property: string, fn: (value: any) => void): this From 94d92c02d02bdd79abc5a037316cf74544bb00a1 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 11 Apr 2024 16:09:13 +0200 Subject: [PATCH 35/36] fix: types --- src/KnxClient.ts | 8 ++- src/KnxProtocol.ts | 93 ++++++++++++++++++---------------- src/types/binary-protocol.d.ts | 8 ++- test/knxproto/test-proto.ts | 22 +++----- 4 files changed, 66 insertions(+), 65 deletions(-) diff --git a/src/KnxClient.ts b/src/KnxClient.ts index 7c81777..6eafe32 100644 --- a/src/KnxClient.ts +++ b/src/KnxClient.ts @@ -5,7 +5,7 @@ import { keyText, KnxConstants } from './KnxConstants' import IpRoutingConnection from './IpRoutingConnection' import IpTunnelingConnection from './IpTunnelingConnection' import KnxLog, { KnxLogOptions } from './KnxLog' -import KnxNetProtocol from './KnxProtocol' +import knxProto from './KnxProtocol' import { Writer } from 'binary-protocol' import { Socket } from 'dgram' import { populateAPDU } from './dptlib' @@ -335,9 +335,7 @@ export class KnxClient extends KnxFSM { onUdpSocketMessage(msg: Buffer, rinfo: any, callback: () => void): void { // get the incoming packet's service type ... try { - const reader = KnxNetProtocol.createReader(msg) - reader.KNXNetHeader('tmp') - const dg = reader.next()['tmp'] as Datagram + const dg = knxProto.parseDatagram(msg) const descr = datagramDesc(dg) KnxLog.get().trace( '(%s): Received %s message: %j', @@ -498,7 +496,7 @@ export class KnxClient extends KnxFSM { send(datagram: Datagram, callback?: (err?: Error) => void): void { let cemitype: string // TODO: set, but unused try { - this.writer = KnxNetProtocol.createWriter() + this.writer = knxProto.createWriter() switch (datagram.service_type) { case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index 4860b11..842492c 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -12,25 +12,34 @@ import { APCICODES, keyText, KnxConstants } from './KnxConstants' import KnxLog from './KnxLog' import type { Datagram } from './KnxClient' -export interface KnxProtocolI extends BinaryProtocol { +export class KnxProtocol extends BinaryProtocol { lengths: { [key: string]: (value: any) => number } + twoLevelAddressing: boolean + debug: boolean + apduStruct: Parser + + parseDatagram(buffer: Buffer): Datagram { + const reader = this.createReader(buffer) + reader.KNXNetHeader('knxnet') + return reader.next().knxnet + } } -const KnxProtocol: KnxProtocolI = new BinaryProtocol() as KnxProtocolI +const proto = new KnxProtocol() // defaults -KnxProtocol.twoLevelAddressing = false -KnxProtocol.lengths = {} // TODO: Can this be a local variable, do we need to expose it? +proto.twoLevelAddressing = false +proto.lengths = {} // TODO: Can this be a local variable, do we need to expose it? // helper function: what is the byte length of an object? const knxlen = (objectName: string, context: any) => { - const lf = KnxProtocol.lengths[objectName] + const lf = proto.lengths[objectName] return typeof lf === 'function' ? lf(context) : lf } -KnxProtocol.define('IPv4Endpoint', { +proto.define('IPv4Endpoint', { read(propertyName: string) { this.pushStack({ addr: null, port: null }) .raw('addr', 4) @@ -57,7 +66,7 @@ KnxProtocol.define('IPv4Endpoint', { }, }) -KnxProtocol.lengths['IPv4Endpoint'] = (value: string) => (value ? 6 : 0) +proto.lengths['IPv4Endpoint'] = (value: string) => (value ? 6 : 0) /* CRI: connection request/response */ // creq[22] = 0x04; /* structure len (4 bytes) */ @@ -65,7 +74,7 @@ KnxProtocol.lengths['IPv4Endpoint'] = (value: string) => (value ? 6 : 0) // creq[24] = 0x02; /* KNX Layer (Tunnel Link Layer) */ // creq[25] = 0x00; /* Reserved */ // ==> 4 bytes -KnxProtocol.define('CRI', { +proto.define('CRI', { read(propertyName: string) { this.pushStack({ header_length: 0, @@ -89,8 +98,8 @@ KnxProtocol.define('CRI', { ) } }) - .popStack(propertyName, (data) => { - if (KnxProtocol.debug) + .popStack(propertyName, (data: Datagram['cri']) => { + if (proto.debug) KnxLog.get().debug(`read CRI: ${JSON.stringify(data)}`) // pop the interim value off the stack and insert the real value into `propertyName` return data @@ -105,17 +114,16 @@ KnxProtocol.define('CRI', { .UInt8(value.unused) }, }) -KnxProtocol.lengths['CRI'] = (value: Datagram['cri']) => (value ? 4 : 0) +proto.lengths['CRI'] = (value: Datagram['cri']) => (value ? 4 : 0) // connection state response/request -KnxProtocol.define('ConnState', { +proto.define('ConnState', { read(propertyName: string) { this.pushStack({ channel_id: null, status: null }) .UInt8('channel_id') .UInt8('status') .popStack(propertyName, (data: any) => { - if (KnxProtocol.debug) - KnxLog.get().trace('read ConnState: %j', data) + if (proto.debug) KnxLog.get().trace('read ConnState: %j', data) return data }) }, @@ -125,11 +133,10 @@ KnxProtocol.define('ConnState', { this.UInt8(value.channel_id).UInt8(value.status) }, }) -KnxProtocol.lengths['ConnState'] = (value: Datagram['connstate']) => - value ? 2 : 0 +proto.lengths['ConnState'] = (value: Datagram['connstate']) => (value ? 2 : 0) // connection state response/request -KnxProtocol.define('TunnState', { +proto.define('TunnState', { read(propertyName: string) { this.pushStack({ header_length: null, @@ -142,7 +149,7 @@ KnxProtocol.define('TunnState', { .UInt8('seqnum') .UInt8('rsvd') .tap((hdr: any) => { - if (KnxProtocol.debug) + if (proto.debug) KnxLog.get().trace('reading TunnState: %j', hdr) switch (hdr.status) { case 0x00: @@ -157,16 +164,14 @@ KnxProtocol.define('TunnState', { return KnxLog.get().error( 'TunnState: cannot write null value for TunnState', ) - if (KnxProtocol.debug) - KnxLog.get().trace('writing TunnState: %j', value) + if (proto.debug) KnxLog.get().trace('writing TunnState: %j', value) this.UInt8(0x04) .UInt8(value.channel_id) .UInt8(value.seqnum) .UInt8(value.rsvd) }, }) -KnxProtocol.lengths['TunnState'] = (value: Datagram['tunnstate']) => - value ? 4 : 0 +proto.lengths['TunnState'] = (value: Datagram['tunnstate']) => (value ? 4 : 0) /* Connection HPAI */ // creq[6] = /* Host Protocol Address Information (HPAI) Lenght */ @@ -181,7 +186,7 @@ KnxProtocol.lengths['TunnState'] = (value: Datagram['tunnstate']) => // creq[16-19] = /* IPv4 address */ // creq[20-21] = /* IPv4 local port number for TUNNELING requests */ // ==> 8 bytes -KnxProtocol.define('HPAI', { +proto.define('HPAI', { read(propertyName: string) { this.pushStack({ header_length: 8, @@ -193,7 +198,7 @@ KnxProtocol.define('HPAI', { .IPv4Endpoint('tunnel_endpoint') .tap(function (hdr: Datagram['hpai']) { if (this.buffer.length < hdr.header_length) { - if (KnxProtocol.debug) + if (proto.debug) KnxLog.get().trace( '%d %d %d', this.buffer.length, @@ -202,7 +207,7 @@ KnxProtocol.define('HPAI', { ) throw Error('Incomplete KNXNet HPAI header') } - if (KnxProtocol.debug) { + if (proto.debug) { KnxLog.get().trace( 'read HPAI: %j, proto = %s', hdr, @@ -226,7 +231,7 @@ KnxProtocol.define('HPAI', { .IPv4Endpoint(value.tunnel_endpoint) }, }) -KnxProtocol.lengths['HPAI'] = (value: Datagram['hpai']) => { +proto.lengths['HPAI'] = (value: Datagram['hpai']) => { return value ? 8 : 0 } @@ -379,9 +384,9 @@ const ctrlStruct = new Parser() .bit4('extendedFrame') // APDU: 2 bytes, tcpi = 6 bits, apci = 4 bits, remaining 6 bits = data (when length=1) -KnxProtocol.apduStruct = new Parser().bit6('tpci').bit4('apci').bit6('data') +proto.apduStruct = new Parser().bit6('tpci').bit4('apci').bit6('data') -KnxProtocol.define('APDU', { +proto.define('APDU', { read(propertyName: string) { this.pushStack({ apdu_length: null, @@ -398,7 +403,7 @@ KnxProtocol.define('APDU', { .tap((hdr: Datagram['cemi']['apdu']) => { // Parse the APDU. tcpi/apci bits split across byte boundary. // Typical example of protocol designed by committee. - const apdu = KnxProtocol.apduStruct.parse(hdr.apdu_raw) + const apdu = proto.apduStruct.parse(hdr.apdu_raw) hdr.tpci = apdu.tpci hdr.apci = APCICODES[apdu.apci] // APDU data should ALWAYS be a buffer, even for 1-bit payloads @@ -406,7 +411,7 @@ KnxProtocol.define('APDU', { hdr.apdu_length > 1 ? hdr.apdu_raw.slice(2) : Buffer.from([apdu.data]) - if (KnxProtocol.debug) + if (proto.debug) KnxLog.get().trace(' unmarshalled APDU: %j', hdr) }) .popStack(propertyName, (data) => data) @@ -448,7 +453,7 @@ KnxProtocol.define('APDU', { /* APDU length is truly chaotic: header and data can be interleaved (but not always!), so that apdu_length=1 means _2_ bytes following the apdu_length */ -KnxProtocol.lengths['APDU'] = (value) => { +proto.lengths['APDU'] = (value) => { if (!value) return 0 // if we have the APDU bitlength, usually by the DPT, then simply use it if (value.bitlength || (value.data && value.data.bitlength)) { @@ -490,7 +495,7 @@ KnxProtocol.lengths['APDU'] = (value) => { ) } -KnxProtocol.define('CEMI', { +proto.define('CEMI', { read(propertyName: string) { this.pushStack({ msgcode: 0, @@ -527,7 +532,7 @@ KnxProtocol.define('CEMI', { case KnxConstants.MESSAGECODES['L_Data.ind']: case KnxConstants.MESSAGECODES['L_Data.con']: { this.APDU('apdu') - if (KnxProtocol.debug) + if (proto.debug) KnxLog.get().trace( '--- unmarshalled APDU ==> %j', hdr.apdu, @@ -539,7 +544,7 @@ KnxProtocol.define('CEMI', { }, write(value: Datagram['cemi']) { if (!value) throw Error('cannot write null CEMI value') - if (KnxProtocol.debug) KnxLog.get().trace('CEMI.write: \n\t%j', value) + if (proto.debug) KnxLog.get().trace('CEMI.write: \n\t%j', value) if (value.ctrl === null) throw Error('no Control Field supplied') const ctrlField1 = value.ctrl.frameType * 0x80 + @@ -557,8 +562,8 @@ KnxProtocol.define('CEMI', { .UInt8(value.addinfo_length) .UInt8(ctrlField1) .UInt8(ctrlField2) - .raw(KnxAddress.parse(value.src_addr, KnxAddress.TYPE.PHYSICAL), 2) - .raw(KnxAddress.parse(value.dest_addr, value.ctrl.destAddrType), 2) + .raw(KnxAddress.parse(value.src_addr, KnxAddress.TYPE.PHYSICAL)) + .raw(KnxAddress.parse(value.dest_addr, value.ctrl.destAddrType)) // only need to marshal an APDU if this is a // L_Data.* (requet/indication/confirmation) switch (value.msgcode) { @@ -572,15 +577,15 @@ KnxProtocol.define('CEMI', { }, }) -KnxProtocol.lengths['CEMI'] = (value: Datagram['cemi']) => { +proto.lengths['CEMI'] = (value: Datagram['cemi']) => { if (!value) return 0 const apdu_length = knxlen('APDU', value.apdu) - if (KnxProtocol.debug) + if (proto.debug) KnxLog.get().trace('knxlen of cemi: %j == %d', value, 8 + apdu_length) return 8 + apdu_length } -KnxProtocol.define('KNXNetHeader', { +proto.define('KNXNetHeader', { read(propertyName: string) { this.pushStack({ header_length: 0, @@ -593,7 +598,7 @@ KnxProtocol.define('KNXNetHeader', { .UInt16BE('service_type') .UInt16BE('total_length') .tap(function (hdr: Datagram) { - if (KnxProtocol.debug) + if (proto.debug) KnxLog.get().trace('read KNXNetHeader :%j', hdr) if (this.buffer.length + hdr.header_length < this.total_length) throw Error( @@ -643,7 +648,7 @@ KnxProtocol.define('KNXNetHeader', { } }) .popStack(propertyName, (data) => { - if (KnxProtocol.debug) + if (proto.debug) KnxLog.get().trace(JSON.stringify(data, null, 4)) return data }) @@ -651,7 +656,7 @@ KnxProtocol.define('KNXNetHeader', { write(value: Datagram) { if (!value) throw Error('cannot write null KNXNetHeader value') value.total_length = knxlen('KNXNetHeader', value) - if (KnxProtocol.debug) KnxLog.get().trace('writing KnxHeader:', value) + if (proto.debug) KnxLog.get().trace('writing KnxHeader:', value) this.UInt8(6) // header length (6 bytes constant) .UInt8(0x10) // protocol version 1.0 .UInt16BE(value.service_type) @@ -694,7 +699,7 @@ KnxProtocol.define('KNXNetHeader', { } }, }) -KnxProtocol.lengths['KNXNetHeader'] = (value: Datagram) => { +proto.lengths['KNXNetHeader'] = (value: Datagram) => { if (!value) throw Error('Must supply a valid KNXNetHeader value') switch (value.service_type) { // case SERVICE_TYPE.SEARCH_REQUEST: @@ -727,4 +732,4 @@ KnxProtocol.lengths['KNXNetHeader'] = (value: Datagram) => { } } -export default KnxProtocol +export default proto diff --git a/src/types/binary-protocol.d.ts b/src/types/binary-protocol.d.ts index cd49d40..0468390 100644 --- a/src/types/binary-protocol.d.ts +++ b/src/types/binary-protocol.d.ts @@ -13,8 +13,11 @@ declare module 'binary-protocol' { tap(callback: (value: any) => void): this enqueue(name: string, arg1: any, arg2: any): this prepend(fn: (arg: any) => void, arg1: any): this + /** Allocate a new object to read the data into */ pushStack(item: any): this + /** Pop the interim value off the stack and insert the real value into `property` */ popStack(property: string, fn: (value: any) => void): this + /** Collect the final data to return */ collect(property: string, fn: (value: any) => void): this loop(property: string, fn: (value: any) => void): this end(fn: () => void): this @@ -24,7 +27,7 @@ declare module 'binary-protocol' { next(chunk?: any): any process(): any createLooper(property: string, fn: (value: any) => void): this - [key: string]: any // method created with `define` + [key: string]: (property: string) => this // method created with `define` } interface Writer { @@ -35,7 +38,7 @@ declare module 'binary-protocol' { raw(buffer: Buffer): this forward(howMany: number): this tap(callback: (value: any) => void): this - [key: string]: any // method created with `define` + [key: string]: (data: any) => this // method created with `define` } interface Commander { @@ -43,6 +46,7 @@ declare module 'binary-protocol' { clone(): this createReadStream(options: any): Reader createWriteStream(options: any): Writer + [key: string]: (data: any) => Promise // method created with `define` } export default class BinaryProtocol { diff --git a/test/knxproto/test-proto.ts b/test/knxproto/test-proto.ts index 1784214..5aaf888 100644 --- a/test/knxproto/test-proto.ts +++ b/test/knxproto/test-proto.ts @@ -3,10 +3,10 @@ * (C) 2016-2018 Elias Karakoulakis */ -import KnxNetProtocol from '../../src/KnxProtocol' +import knxProto from '../../src/KnxProtocol' import test from 'tape' -KnxNetProtocol.debug = true +knxProto.debug = true // test('KNX protocol unmarshaller', function (t) { @@ -19,10 +19,7 @@ test('KNX protocol unmarshaller', function (t) { Object.keys(tests).forEach((key, idx) => { const buf = tests[key] // unmarshal from a buffer... - const reader = KnxNetProtocol.createReader(buf) - const writer = KnxNetProtocol.createWriter() - reader.KNXNetHeader('tmp') - const decoded = reader.next()['tmp'] + const decoded = knxProto.parseDatagram(buf) console.log('\n=== %s: %j ===> %j', key, buf, decoded) t.ok(decoded !== undefined, `${key}: unmarshaled KNX datagram`) }) @@ -66,10 +63,8 @@ test('KNX protocol marshal+unmarshal', function (t) { Object.keys(tests).forEach((key, idx) => { const buf = tests[key] // unmarshal from a buffer... - const reader = KnxNetProtocol.createReader(buf) - const writer = KnxNetProtocol.createWriter() - reader.KNXNetHeader('tmp') - const decoded = reader.next()['tmp'] + const writer = knxProto.createWriter() + const decoded = knxProto.parseDatagram(buf) console.log('\n=== %s: %j ===> %j', key, buf, decoded) t.ok(decoded !== undefined, `${key}: unmarshaled KNX datagram`) // then marshal the datagram again into a buffer... @@ -260,13 +255,11 @@ test('KNX protocol marshaller', function (t) { : testcase.hexbuf console.log('\n=== %s', key) // marshal the test datagram - const writer = KnxNetProtocol.createWriter() + const writer = knxProto.createWriter() writer.KNXNetHeader(testcase.dgram) if (Buffer.compare(buf, writer.buffer) !== 0) { // if this fails, unmarshal the buffer again to a datagram - const reader = KnxNetProtocol.createReader(writer.buffer) - reader.KNXNetHeader('tmp') - const decoded = reader.next()['tmp'] + const decoded = knxProto.parseDatagram(writer.buffer) console.log( '\n\n========\n FAIL: %s\n========\nbuffer is different:\n', key, @@ -276,6 +269,7 @@ test('KNX protocol marshaller', function (t) { ) console.log('expected : %s', buf.toString('hex')) console.log('got instead: %s', writer.buffer.toString('hex')) + console.log('decoded : %j', decoded) } t.ok(Buffer.compare(buf, writer.buffer) === 0) }) From 3233f6d1dee282714057021565644f032a704fe1 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 11 Apr 2024 18:03:20 +0200 Subject: [PATCH 36/36] fix: add constants --- src/KnxConstants.ts | 2 ++ src/KnxProtocol.ts | 13 ++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/KnxConstants.ts b/src/KnxConstants.ts index 9499a80..384f8dd 100644 --- a/src/KnxConstants.ts +++ b/src/KnxConstants.ts @@ -104,6 +104,8 @@ export const KnxConstants = { FRAMETYPE, RESPONSECODE, MESSAGECODES, + HEADER_SIZE_10: 0x6, + KNXNETIP_VERSION_10: 0x10, } /* TODO helper function to print enum keys */ diff --git a/src/KnxProtocol.ts b/src/KnxProtocol.ts index 842492c..8a4b8c6 100644 --- a/src/KnxProtocol.ts +++ b/src/KnxProtocol.ts @@ -657,12 +657,11 @@ proto.define('KNXNetHeader', { if (!value) throw Error('cannot write null KNXNetHeader value') value.total_length = knxlen('KNXNetHeader', value) if (proto.debug) KnxLog.get().trace('writing KnxHeader:', value) - this.UInt8(6) // header length (6 bytes constant) - .UInt8(0x10) // protocol version 1.0 + this.UInt8(KnxConstants.HEADER_SIZE_10) // header length (6 bytes constant) + .UInt8(KnxConstants.KNXNETIP_VERSION_10) // protocol version 1.0 .UInt16BE(value.service_type) .UInt16BE(value.total_length) switch (value.service_type) { - // case SERVICE_TYPE.SEARCH_REQUEST: case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: { if (value.hpai) this.HPAI(value.hpai) if (value.tunn) this.HPAI(value.tunn) @@ -705,7 +704,7 @@ proto.lengths['KNXNetHeader'] = (value: Datagram) => { // case SERVICE_TYPE.SEARCH_REQUEST: case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: return ( - 6 + + KnxConstants.HEADER_SIZE_10 + knxlen('HPAI', value.hpai) + knxlen('HPAI', value.tunn) + knxlen('CRI', value.cri) @@ -715,7 +714,7 @@ proto.lengths['KNXNetHeader'] = (value: Datagram) => { case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_RESPONSE: case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: return ( - 6 + + KnxConstants.HEADER_SIZE_10 + knxlen('ConnState', value.connstate) + knxlen('HPAI', value.hpai) + knxlen('CRI', value.cri) @@ -723,12 +722,12 @@ proto.lengths['KNXNetHeader'] = (value: Datagram) => { case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: return ( - 6 + + KnxConstants.HEADER_SIZE_10 + knxlen('TunnState', value.tunnstate) + knxlen('CEMI', value.cemi) ) case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: - return 6 + knxlen('CEMI', value.cemi) + return KnxConstants.HEADER_SIZE_10 + knxlen('CEMI', value.cemi) } }