├── .gitignore ├── typescript-sample ├── .gitignore ├── package.json ├── README.md ├── tsconfig.json ├── test-toggle-onoff.ts └── tslint.json ├── run-wired-tests.sh ├── src ├── devices │ ├── index.js │ └── BinarySwitch.js ├── dptlib │ ├── dpt15.js │ ├── dpt17.js │ ├── dpt12.js │ ├── dpt6.js │ ├── dpt20.js │ ├── dpt16.js │ ├── dpt232.js │ ├── dpt4.js │ ├── dpt19.js │ ├── dpt5.js │ ├── dpt238.js │ ├── dpt13.js │ ├── dpt8.js │ ├── dpt11.js │ ├── dpt21.js │ ├── dpt237.js │ ├── dpt3.js │ ├── dpt7.js │ ├── dpt18.js │ ├── dpt10.js │ ├── dpt2.js │ ├── dpt14.js │ ├── dpt1.js │ ├── dpt9.js │ └── index.js ├── KnxLog.js ├── IpTunnelingConnection.js ├── IpRoutingConnection.js ├── KnxConstants.js ├── Datapoint.js ├── Address.js ├── Connection.js └── FSM.js ├── .npmignore ├── AUTHORS ├── test ├── dptlib │ ├── test-dpt4.js │ ├── test-dpt1.js │ ├── test-dpt6.js │ ├── test-dpt7.js │ ├── test-dpt8.js │ ├── test-dpt2.js │ ├── test-dpt12.js │ ├── test-dpt3.js │ ├── test-dpt9.js │ ├── test-dpt13.js │ ├── test-dpt5.js │ ├── test-dpt21.js │ ├── test-dpt11.js │ ├── test-dpt19.js │ ├── test-dpt237.js │ ├── test-dpt.js │ ├── test-dpt10.js │ └── commontest.js ├── connection │ └── test-connect-routing.js ├── wiredtests │ ├── wiredtest-options.js │ ├── test-connect-routing-hybrid.js │ ├── test-logotimer.js │ ├── test-read.js │ ├── test-connect-tunnel.js │ ├── test-readstorm.js │ └── test-control.js ├── datapoint │ └── test-datapoint.js └── knxproto │ ├── test-address.js │ └── test-proto.js ├── index.js ├── README-knxd.md ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── manualtest ├── test-writestorm.js └── test-toggle.js ├── README-events.md ├── package.json ├── README.md ├── README-datapoints.md ├── README-resilience.md ├── index.d.ts └── README-API.md /.gitignore: -------------------------------------------------------------------------------- 1 | .tags* 2 | node_modules 3 | -------------------------------------------------------------------------------- /typescript-sample/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /run-wired-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | WIREDTEST=1 npm test 3 | -------------------------------------------------------------------------------- /src/devices/index.js: -------------------------------------------------------------------------------- 1 | exports.BinarySwitch = require('./BinarySwitch.js'); 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | manualtest 3 | bitbucket-pipelines.yml 4 | README*.md 5 | typescript-sample 6 | run-wired-tests.sh -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Elias Karakoulakis 2 | Mattias Holmlund 3 | Timo Müller -------------------------------------------------------------------------------- /test/dptlib/test-dpt4.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | require('./commontest').do('DPT4', [ 6 | { apdu_data: [0x40], jsval: "@"}, 7 | { apdu_data: [0x76], jsval: "v"} 8 | ]); 9 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | require('./commontest').do('DPT1', [ 6 | { apdu_data: [0x00], jsval: false}, 7 | { apdu_data: [0x01], jsval: true} 8 | ]); 9 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | require('./commontest').do('DPT6', [ 7 | { apdu_data: [0x00], jsval: 0}, 8 | { apdu_data: [0x7f], jsval: 127}, 9 | { apdu_data: [0x80], jsval: -128}, 10 | { apdu_data: [0xff], jsval: -1} 11 | ]); 12 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt7.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | require('./commontest').do('DPT7', [ 7 | { apdu_data: [0x00, 0x11], jsval: 17}, 8 | { apdu_data: [0x01, 0x00], jsval: 256}, 9 | { apdu_data: [0x10, 0x01], jsval: 4097}, 10 | { apdu_data: [0xff, 0xff], jsval: 65535}, 11 | ]); 12 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt8.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | require('./commontest').do('DPT8', [ 7 | { apdu_data: [0x00, 0x11], jsval: 17}, 8 | { apdu_data: [0x01, 0x00], jsval: 256}, 9 | { apdu_data: [0x7f, 0xff], jsval: 32767}, 10 | { apdu_data: [0x80, 0x00], jsval: -32768}, 11 | { apdu_data: [0xff, 0xff], jsval: -1} 12 | ]); 13 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | require('./commontest').do('DPT2', [ 7 | { apdu_data: [0x00], jsval: {priority: 0 , data: 0}}, 8 | { apdu_data: [0x01], jsval: {priority: 0 , data: 1}}, 9 | { apdu_data: [0x02], jsval: {priority: 1 , data: 0}}, 10 | { apdu_data: [0x03], jsval: {priority: 1 , data: 1}} 11 | ]); 12 | -------------------------------------------------------------------------------- /typescript-sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-sample", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tslint *.ts; tsc" 9 | }, 10 | "author": "Mattias Holmlund ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "knx": "../", 14 | "tslint": "^5.1.0", 15 | "typescript": "^2.2.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /typescript-sample/README.md: -------------------------------------------------------------------------------- 1 | # Typescript sample for knx.js 2 | 3 | This directory contains a sample application in typescript using knx.js 4 | 5 | ## Usage 6 | 7 | cd typescript-sample 8 | npm install 9 | npm run build 10 | node test-toggle-onoff.js 13/0/137 11 | 12 | If you have a knx gateway that doesn't support IP multicast, set the environment variable 13 | KNXGW to the IP-address of your gateway: 14 | 15 | KNXGW=192.168.1.17 node test-toggle-onoff.js 13/0/137 16 | -------------------------------------------------------------------------------- /src/dptlib/dpt15.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT15.*: Access data 8 | // 9 | 10 | // TODO: implement fromBuffer, formatAPDU 11 | 12 | // DPT15 base type info 13 | exports.basetype = { 14 | "bitlength" : 32, 15 | "valuetype" : "basic", 16 | "desc" : "4-byte access control data" 17 | } 18 | 19 | // DPT15 subtypes info 20 | exports.subtypes = { 21 | "000" : { 22 | "name" : "DPT_Access_Data" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/dptlib/dpt17.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT17: Scene number 8 | // 9 | 10 | // TODO: implement fromBuffer, formatAPDU 11 | 12 | // DPT17 basetype info 13 | exports.basetype = { 14 | bitlength : 8, 15 | valuetype : 'basic', 16 | desc : "scene number" 17 | } 18 | 19 | // DPT17 subtypes 20 | exports.subtypes = { 21 | // 17.001 Scene number 22 | "001" : { use : "G", 23 | name : "DPT_SceneNumber", desc : "Scene Number", 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /typescript-sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2016", 5 | "noImplicitAny": true, 6 | "noImplicitReturns": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "sourceMap": false, 10 | "strictNullChecks": true, 11 | "moduleResolution": "node", 12 | "preserveConstEnums": true 13 | }, 14 | "include": [ 15 | "*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } -------------------------------------------------------------------------------- /src/dptlib/dpt12.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT12.*: 4-byte unsigned value 8 | // 9 | 10 | 11 | // DPT12 base type info 12 | exports.basetype = { 13 | bitlength : 32, 14 | signedness: "unsigned", 15 | valuetype : "basic", 16 | desc : "4-byte unsigned value" 17 | } 18 | 19 | // DPT12 subtype info 20 | exports.subtypes = { 21 | // 12.001 counter pulses 22 | "001" : { 23 | "name" : "DPT_Value_4_Ucount", "desc" : "counter pulses" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt12.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | require('./commontest').do('DPT12', [ 7 | { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17}, 8 | { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256}, 9 | { apdu_data: [0x00, 0x00, 0x10, 0x01], jsval: 4097}, 10 | { apdu_data: [0x00, 0x00, 0xff, 0xff], jsval: 65535}, 11 | { apdu_data: [0x00, 0x01, 0x00, 0x00], jsval: 65536}, 12 | { apdu_data: [0x07, 0x5b, 0xcd, 0x15], jsval: 123456789}, 13 | { apdu_data: [0x49, 0x96, 0x02, 0xd2], jsval: 1234567890}, 14 | ]); 15 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | const commontest = require('./commontest') 6 | 7 | commontest.do('DPT3', [ 8 | { apdu_data: [0x00], jsval: {decr_incr: 0, data: 0}}, 9 | { apdu_data: [0x06], jsval: {decr_incr: 0, data: 6}} 10 | ]); 11 | 12 | commontest.do('DPT3.007', [ 13 | { apdu_data: [0x01], jsval: {decr_incr: 0, data: 1}}, 14 | { apdu_data: [0x05], jsval: {decr_incr: 0, data: 5}}, 15 | { apdu_data: [0x08], jsval: {decr_incr: 1, data: 0}}, 16 | { apdu_data: [0x0f], jsval: {decr_incr: 1, data: 7}} 17 | ]); 18 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt9.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | require('./commontest').do('DPT9', [ 7 | { apdu_data: [0x00, 0x02], jsval: 0.02}, 8 | { apdu_data: [0x87, 0xfe], jsval: -0.02}, 9 | { apdu_data: [0x02, 0xf8], jsval: 7.6}, 10 | { apdu_data: [0x0c, 0x24], jsval: 21.2}, 11 | { apdu_data: [0x0c, 0x7e], jsval: 23}, 12 | { apdu_data: [0x5c, 0xc4], jsval: 24985.6}, 13 | { apdu_data: [0xdb, 0x3c], jsval: -24985.6}, 14 | { apdu_data: [0x7f, 0xfe], jsval: 670433.28}, 15 | { apdu_data: [0xf8, 0x02], jsval: -670433.28}, 16 | ]); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2017 Elias Karakoulakis 4 | */ 5 | 6 | const path = require('path'); 7 | const util = require('util'); 8 | const log = require('log-driver').logger; 9 | 10 | const knx_path = path.join(__dirname, 'package.json'); 11 | const pkginfo = require(knx_path); 12 | 13 | log.info(util.format('Loading %s: %s, version: %s', 14 | pkginfo.name, pkginfo.description, pkginfo.version)); 15 | 16 | exports.Connection = require('./src/Connection.js'); 17 | exports.Datapoint = require('./src/Datapoint.js'); 18 | exports.Devices = require('./src/devices'); 19 | exports.Log = require('./src/KnxLog.js'); 20 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt13.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | require('./commontest').do('DPT13', [ 7 | { apdu_data: [0x00, 0x00, 0x00, 0x11], jsval: 17}, 8 | { apdu_data: [0x00, 0x00, 0x01, 0x00], jsval: 256}, 9 | { apdu_data: [0x00, 0x00, 0x10, 0x00], jsval: 4096}, 10 | { apdu_data: [0x00, 0x01, 0x00, 0x00], jsval: 65536}, 11 | { apdu_data: [0x7f, 0xff, 0xff, 0xff], jsval: 2147483647}, 12 | { apdu_data: [0x80, 0x00, 0x00, 0x00], jsval: -2147483648}, 13 | { apdu_data: [0x80, 0x00, 0x00, 0x01], jsval: -2147483647}, 14 | { apdu_data: [0xff, 0xff, 0xff, 0xff], jsval: -1}, 15 | ]); 16 | -------------------------------------------------------------------------------- /src/dptlib/dpt6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // Bitstruct to parse a DPT6 frame (8-bit signed integer) 7 | // Always 8-bit aligned. 8 | 9 | // DPT Basetype info 10 | exports.basetype = { 11 | "bitlength" : 8, 12 | "signedness": "signed", 13 | "valuetype" : "basic", 14 | "desc" : "8-bit signed value", 15 | "range" : [-128, 127] 16 | } 17 | 18 | // DPT subtypes info 19 | exports.subtypes = { 20 | // 6.001 percentage (-128%..127%) 21 | "001" : { 22 | "name" : "DPT_Switch", "desc" : "percent", 23 | "unit" : "%", 24 | }, 25 | 26 | // 6.002 counter pulses (-128..127) 27 | "010" : { 28 | "name" : "DPT_Bool", "desc" : "counter pulses", 29 | "unit" : "pulses" 30 | }, 31 | 32 | // 33 | } 34 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | const commontest = require('./commontest'); 6 | // DPT5 without subtype: no scaling 7 | commontest.do('DPT5', [ 8 | { apdu_data: [0x00], jsval: 0}, 9 | { apdu_data: [0x40], jsval: 64}, 10 | { apdu_data: [0x41], jsval: 65}, 11 | { apdu_data: [0x80], jsval: 128}, 12 | { apdu_data: [0xff], jsval: 255} 13 | ]); 14 | // 5.001 percentage (0=0..ff=100%) 15 | commontest.do('DPT5.001', [ 16 | { apdu_data: [0x00], jsval: 0 }, 17 | { apdu_data: [0x80], jsval: 50}, 18 | { apdu_data: [0xff], jsval: 100} 19 | ]); 20 | // 5.003 angle (degrees 0=0, ff=360) 21 | commontest.do('DPT5.003', [ 22 | { apdu_data: [0x00], jsval: 0 }, 23 | { apdu_data: [0x80], jsval: 181 }, 24 | { apdu_data: [0xff], jsval: 360 } 25 | ]); 26 | -------------------------------------------------------------------------------- /README-knxd.md: -------------------------------------------------------------------------------- 1 | **Please note**: if you use `eibd` or `knxd` as your IP router, and you have it running on the **same** box as your NodeJS app, then eibd/knxd will DROP multicast packets coming from the same source IP address. This is meant to prevent endless loops: if by error knxd is configured also as a client (that will join the same multicast group), then a multicast storm will flood your LAN. I'm sure you've experienced something similar if you take a microphone connected to an amplifier and put it near the speakers (ECHO!) 2 | 3 | `Layer 0 [11:server/Server 7.133] Dropped(017): 06 10 05 30 00 11 29 00 BC D0 11 64 29 0F 01 00 80` 4 | 5 | The trick here (although not entirely within the specs) is to use the loopback interface, so if you define the KNX connection address/port to `127.0.0.1:3671` then this will bypass the source address check (and happily route your packets down your USB or TPUART interface). 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 20.x] 15 | fail-fast: false 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | 25 | - name: Install Dependencies 26 | run: npm ci 27 | 28 | # - name: Lint 29 | # if: matrix.node-version == '20.x' 30 | # # only run on latest node version, no reason to run on all 31 | # run: | 32 | # npm run lint 33 | 34 | - name: Test NodeJS 35 | run: npm run test 36 | timeout-minutes: 5 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/dptlib/dpt20.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT20: 1-byte HVAC 10 | // 11 | // FIXME: help needed 12 | exports.formatAPDU = (value) => { 13 | log.debug('./knx/src/dpt20.js : input value = ' + value); 14 | return Buffer.from([value]); 15 | }; 16 | 17 | exports.fromBuffer = (buf) => { 18 | if (buf.length !== 1) throw 'Buffer should be 1 bytes long'; 19 | const ret = buf.readUInt8(0); 20 | log.debug(' dpt20.js fromBuffer : ' + ret); 21 | return ret; 22 | }; 23 | 24 | exports.basetype = { 25 | bitlength: 8, 26 | range: [,], 27 | valuetype: 'basic', 28 | desc: '1-byte', 29 | }; 30 | 31 | exports.subtypes = { 32 | // 20.102 HVAC mode 33 | 102: { 34 | name: 'HVAC_Mode', 35 | desc: '', 36 | unit: '', 37 | scalar_range: [,], 38 | range: [,], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/KnxLog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | const util = require('util'); 6 | let logger; 7 | 8 | const create = (options) => { 9 | const level = 10 | (options && (options.debug ? 'debug' : options.loglevel)) || 'info'; 11 | //console.trace('new logger, level='+lvl); 12 | return require('log-driver')({ 13 | level, 14 | format(lvl, msg /*string*/, ...a) { 15 | // lvl is the log level ie 'debug' 16 | const ts = new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''); 17 | return a.length 18 | ? // if more than one item to log, assume msg is a fmt string 19 | util.format('[%s] %s ' + msg, lvl, ts, ...a) 20 | : // otherwise, msg is a plain string 21 | util.format('[%s] %s %s', lvl, ts, msg); 22 | }, 23 | }); 24 | }; 25 | 26 | module.exports = { 27 | get: (options) => logger || (logger = create(options)), 28 | }; 29 | -------------------------------------------------------------------------------- /test/connection/test-connect-routing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | Error.stackTraceLimit = Infinity; 7 | 8 | const knx = require('../..'); 9 | const address = require('../../src/Address.js'); 10 | const assert = require('assert'); 11 | const test = require('tape'); 12 | 13 | // 14 | test('KNX connect routing', function(t) { 15 | var connection = knx.Connection({ 16 | loglevel: 'trace', 17 | handlers: { 18 | connected: function() { 19 | console.log('----------'); 20 | console.log('Connected!'); 21 | console.log('----------'); 22 | t.pass('connected in routing mode'); 23 | t.end(); 24 | process.exit(0); 25 | }, 26 | error: function() { 27 | t.fail('error connecting'); 28 | t.end(); 29 | process.exit(1); 30 | } 31 | } 32 | }); 33 | }); 34 | 35 | setTimeout(function() { 36 | console.log('Exiting with timeout...'); 37 | process.exit(2); 38 | }, 1000); 39 | -------------------------------------------------------------------------------- /typescript-sample/test-toggle-onoff.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2017 Elias Karakoulakis 4 | */ 5 | 6 | import * as knx from 'knx' 7 | 8 | var groupAddress = process.argv[2] 9 | 10 | var connection = new knx.Connection({ 11 | ipAddr: process.env.KNXGW, 12 | handlers: { 13 | connected: onConnected 14 | } 15 | }) 16 | 17 | async function onConnected() { 18 | console.log('Connected') 19 | var dp = new knx.Datapoint({ 20 | ga: groupAddress, 21 | dpt: 'DPT1.001' 22 | }, connection) 23 | 24 | dp.on('change', (oldValue: number, newValue: number) => 25 | console.log(`Value changed from ${oldValue} to ${newValue}`) ) 26 | 27 | dp.read() 28 | await wait(2000) 29 | 30 | dp.write(1) 31 | await wait(2000) 32 | 33 | dp.write(0) 34 | await wait(2000) 35 | 36 | connection.Disconnect() 37 | } 38 | 39 | function wait(ms: number) { 40 | return new Promise( (resolve) => { 41 | setTimeout(resolve, ms) 42 | }) 43 | } -------------------------------------------------------------------------------- /src/dptlib/dpt16.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT16: ASCII string (max 14 chars) 10 | // 11 | 12 | exports.formatAPDU = (value) => { 13 | if (typeof value !== 'string') return log.warn('Must supply a string value'); 14 | 15 | const buf = Buffer.alloc(14); 16 | buf.write(value, 'ascii'); 17 | return buf; 18 | }; 19 | 20 | exports.fromBuffer = (buf) => buf.toString('ascii'); 21 | // DPT16 basetype info 22 | exports.basetype = { 23 | bitlength: 14 * 8, 24 | valuetype: 'basic', 25 | desc: '14-character string', 26 | }; 27 | 28 | // DPT9 subtypes 29 | exports.subtypes = { 30 | // 16.000 ASCII string 31 | '000': { 32 | use: 'G', 33 | name: 'DPT_String_ASCII', 34 | desc: 'ASCII string', 35 | force_encoding: 'US-ASCII', 36 | }, 37 | 38 | // 16.001 ISO-8859-1 string 39 | '001': { 40 | use: 'G', 41 | name: 'DPT_String_8859_1', 42 | desc: 'ISO-8859-1 string', 43 | force_encoding: 'ISO-8859-1', 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Elias Karakoulakis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/wiredtests/wiredtest-options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | /* 7 | define the required options for running wired tests 8 | */ 9 | module.exports = { 10 | loglevel: 'trace', 11 | // your KNX IP router UNICAST ip address 12 | ipAddr: '192.168.8.4', 13 | // the physical address used by the wired tests 14 | physAddr: '14.14.14', 15 | // a DPT1 group address pair (binary status) to test write/read/respond 16 | dpt1_status_ga: '1/1/1', 17 | dpt1_control_ga: '1/1/101', 18 | // a DPT9 group address (temperature) that should be able to respond to a GroupValue_Read request 19 | dpt9_temperature_status_ga: '0/0/15', 20 | // a DPT 9 control and its status GA 21 | dpt9_timer_control_ga: '4/1/4', 22 | dpt9_timer_status_ga: '4/1/3', 23 | // a DPT1 group address that should also be able to respond to a GroupValue_Read request 24 | wired_test_control_ga: '5/0/0', 25 | // read storm test: read back statuses from an actuator with multiple relays 26 | readstorm_control_ga_start: '1/1/0', 27 | readstorm_status_ga_start: '1/1/100', 28 | readstorm_range: 8 29 | } 30 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt21.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | const commontest = require('./commontest') 6 | 7 | commontest.do('DPT21', [ 8 | { apdu_data: [0x00], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, 9 | { apdu_data: [0x01], jsval: {outofservice: 1, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, 10 | { apdu_data: [0x03], jsval: {outofservice: 1, fault: 1, overridden: 0, inalarm: 0, alarmunack: 0}}, 11 | { apdu_data: [0x05], jsval: {outofservice: 1, fault: 0, overridden: 1, inalarm: 0, alarmunack: 0}}, 12 | { apdu_data: [0x08], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 1, alarmunack: 0}} 13 | ]); 14 | 15 | commontest.do('DPT21.001', [ 16 | { apdu_data: [0x01], jsval: {outofservice: 1, fault: 0, overridden: 0, inalarm: 0, alarmunack: 0}}, 17 | { apdu_data: [0x05], jsval: {outofservice: 1, fault: 0, overridden: 1, inalarm: 0, alarmunack: 0}}, 18 | { apdu_data: [0x06], jsval: {outofservice: 0, fault: 1, overridden: 1, inalarm: 0, alarmunack: 0}}, 19 | { apdu_data: [0x08], jsval: {outofservice: 0, fault: 0, overridden: 0, inalarm: 1, alarmunack: 0}} 20 | ]); 21 | -------------------------------------------------------------------------------- /src/dptlib/dpt232.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2019 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT232: 3-byte RGB color array 10 | // MSB: Red, Green, LSB: Blue 11 | // 12 | exports.formatAPDU = (value) => { 13 | if (value == null) return log.error('DPT232: cannot write null value'); 14 | 15 | if (typeof value === 'object') { 16 | const { red, green, blue } = value; 17 | if ( 18 | red >= 0 && 19 | red <= 255 && 20 | green >= 0 && 21 | green <= 255 && 22 | blue >= 0 && 23 | blue <= 255 24 | ) 25 | return Buffer.from([red, green, blue]); 26 | } 27 | log.error( 28 | 'DPT232: Must supply an value {red:0..255, green:0.255, blue:0.255}' 29 | ); 30 | }; 31 | 32 | exports.fromBuffer = (buf) => { 33 | const [red, green, blue] = buf; 34 | return { red, green, blue }; 35 | }; 36 | 37 | exports.basetype = { 38 | bitlength: 3 * 8, 39 | valuetype: 'basic', 40 | desc: 'RGB array', 41 | }; 42 | 43 | exports.subtypes = { 44 | 600: { 45 | name: 'RGB', 46 | desc: 'RGB color triplet', 47 | unit: '', 48 | scalar_range: [,], 49 | range: [,], 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /test/datapoint/test-datapoint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | Error.stackTraceLimit = Infinity; 7 | 8 | const knx = require('../..'); 9 | const test = require('tape'); 10 | 11 | // 12 | test('Datapoint', function(t) { 13 | t.throws(() => { 14 | new knx.Datapoint(); 15 | }, null, `must supply at least { ga, dpt }!`); 16 | 17 | class FakeConnection { 18 | constructor() { 19 | this.callbacks = []; 20 | } 21 | on(event, callback) { 22 | this.callbacks.push(callback); 23 | } 24 | emit(evt, src, buf) { 25 | for (const callback of this.callbacks) { 26 | callback(evt, src, buf); 27 | } 28 | } 29 | }; 30 | const conn = new FakeConnection(); 31 | const datapoint = new knx.Datapoint({ga: '1/0/1'}, conn); 32 | datapoint.on('event', (event, value, src) => { 33 | t.ok(event == 'GroupValue_Write', 'event should match GroupValue_Write'); 34 | t.ok(value == false, 'value should be false'); 35 | t.ok(src == '1.1.1', 'src should be 1.1.1'); 36 | }); 37 | conn.emit('GroupValue_Write', '1.1.1', Buffer.from([0x00], 'hex')); 38 | t.end(); 39 | }); 40 | -------------------------------------------------------------------------------- /src/dptlib/dpt4.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT4: 8-bit character 10 | // 11 | exports.formatAPDU = (value) => { 12 | if (value == null) return log.warn('DPT4: cannot write null value'); 13 | 14 | if (typeof value !== 'string') 15 | return log.warn('DPT4: Must supply a character or string'); 16 | 17 | const apdu_data = value.charCodeAt(0); 18 | if (apdu_data > 255) return log.warn('DPT4: must supply an ASCII character'); 19 | 20 | return Buffer.from([apdu_data]); 21 | }; 22 | 23 | exports.fromBuffer = (buf) => { 24 | if (buf.length != 1) return log.warn('DPT4: Buffer should be 1 byte long'); 25 | 26 | return String.fromCharCode(buf[0]); 27 | }; 28 | 29 | exports.basetype = { 30 | bitlength: 8, 31 | valuetype: 'basic', 32 | desc: '8-bit character', 33 | }; 34 | 35 | exports.subtypes = { 36 | // 4.001 character (ASCII) 37 | '001': { 38 | name: 'DPT_Char_ASCII', 39 | desc: 'ASCII character (0-127)', 40 | range: [0, 127], 41 | use: 'G', 42 | }, 43 | // 4.002 character (ISO-8859-1) 44 | '002': { 45 | name: 'DPT_Char_8859_1', 46 | desc: 'ISO-8859-1 character (0..255)', 47 | use: 'G', 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/IpTunnelingConnection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const dgram = require('dgram'); 7 | const KnxLog = require('./KnxLog.js'); 8 | 9 | function IpTunnelingConnection(instance) { 10 | const log = KnxLog.get(); 11 | 12 | instance.BindSocket = function (cb) { 13 | const udpSocket = dgram.createSocket('udp4'); 14 | udpSocket.bind(() => { 15 | log.debug( 16 | 'IpTunnelingConnection.BindSocket %s:%d', 17 | instance.localAddress, 18 | udpSocket.address().port 19 | ); 20 | cb && cb(udpSocket); 21 | }); 22 | return udpSocket; 23 | }; 24 | 25 | instance.Connect = function () { 26 | this.localAddress = this.getLocalAddress(); 27 | // create the socket 28 | this.socket = this.BindSocket((socket) => { 29 | socket.on('error', (errmsg) => log.debug('Socket error: %j', errmsg)); 30 | socket.on('message', (msg, rinfo, callback) => { 31 | log.debug('Inbound message: %s', msg.toString('hex')); 32 | this.onUdpSocketMessage(msg, rinfo, callback); 33 | }); 34 | // start connection sequence 35 | this.transition('connecting'); 36 | }); 37 | return this; 38 | }; 39 | 40 | return instance; 41 | } 42 | 43 | module.exports = IpTunnelingConnection; 44 | -------------------------------------------------------------------------------- /src/dptlib/dpt19.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // TODO: implement fromBuffer, formatAPDU 9 | 10 | // 11 | // DPT19: 8-byte Date and Time 12 | // 13 | 14 | exports.formatAPDU = (value) => { 15 | if (!(value instanceof Date)) 16 | return log.error('DPT19: Must supply a Date object'); 17 | 18 | // Sunday is 0 in Javascript, but 7 in KNX. 19 | const day = value.getDay() === 0 ? 7 : value.getDay(); 20 | return Buffer.from([ 21 | value.getFullYear() - 1900, 22 | value.getMonth() + 1, 23 | value.getDate(), 24 | (day << 5) + value.getHours(), 25 | value.getMinutes(), 26 | value.getSeconds(), 27 | 0, 28 | 0, 29 | ]); 30 | }; 31 | 32 | exports.fromBuffer = (buf) => { 33 | if (buf.length !== 8) return log.warn('DPT19: Buffer should be 8 bytes long'); 34 | return new Date( 35 | buf[0] + 1900, 36 | buf[1] - 1, 37 | buf[2], 38 | buf[3] & 0b00011111, 39 | buf[4], 40 | buf[5] 41 | ); 42 | }; 43 | 44 | exports.basetype = { 45 | bitlength: 64, 46 | valuetype: 'composite', 47 | desc: '8-byte Date+Time', 48 | }; 49 | 50 | exports.subtypes = { 51 | // 19.001 52 | '001': { 53 | name: 'DPT_DateTime', 54 | desc: 'datetime', 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /test/wiredtests/test-connect-routing-hybrid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | Error.stackTraceLimit = Infinity; 6 | 7 | const knx = require('../..'); 8 | const address = require('../../src/Address.js'); 9 | const assert = require('assert'); 10 | const test = require('tape'); 11 | 12 | const options = require('./wiredtest-options.js'); 13 | 14 | /* 15 | ========== ================== 16 | this is a WIRED test and requires a real KNX IP router on the LAN 17 | ========== ================== 18 | to run all tests: $ WIREDTEST=1 npm test 19 | to run one test : $ WIREDTEST=1 node test/wiredtests/.js 20 | */ 21 | if (process.env.hasOwnProperty('WIREDTEST')) { 22 | // 23 | test('KNX connect routing hybrid', function(t) { 24 | var connection = knx.Connection({ 25 | loglevel: 'debug', 26 | forceTunneling: true, 27 | handlers: { 28 | connected: function() { 29 | t.pass('connected in hybrid mode'); 30 | t.end(); 31 | process.exit(0); 32 | }, 33 | error: function(connstatus) { 34 | t.fail('error connecting: '+connstatus); 35 | t.end(); 36 | process.exit(1); 37 | } 38 | } 39 | }); 40 | }); 41 | 42 | setTimeout(function() { 43 | console.log('Exiting with timeout...'); 44 | process.exit(2); 45 | }, 1000); 46 | } 47 | -------------------------------------------------------------------------------- /src/dptlib/dpt5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT5: 8-bit unsigned value 8 | // 9 | // DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) 10 | exports.basetype = { 11 | "bitlength" : 8, 12 | "signedness": "unsigned", 13 | "range" : [0, 255], 14 | "valuetype" : "basic", 15 | "desc" : "8-bit unsigned value" 16 | } 17 | 18 | exports.subtypes = { 19 | // 5.001 percentage (0=0..ff=100%) 20 | "001" : { 21 | "name" : "DPT_Scaling", "desc" : "percent", 22 | "unit" : "%", "scalar_range" : [0, 100] 23 | }, 24 | 25 | // 5.003 angle (degrees 0=0, ff=360) 26 | "003" : { 27 | "name" : "DPT_Angle", "desc" : "angle degrees", 28 | "unit" : "°", "scalar_range" : [0, 360] 29 | }, 30 | 31 | // 5.004 percentage (0..255%) 32 | "004" : { 33 | "name" : "DPT_Percent_U8", "desc" : "percent", 34 | "unit" : "%", 35 | }, 36 | 37 | // 5.005 ratio (0..255) 38 | "005" : { 39 | "name" : "DPT_DecimalFactor", "desc" : "ratio", 40 | "unit" : "ratio", 41 | }, 42 | 43 | // 5.006 tariff (0..255) 44 | "006" : { 45 | "name" : "DPT_Tariff", "desc" : "tariff", 46 | "unit" : "tariff", 47 | }, 48 | 49 | // 5.010 counter pulses (0..255) 50 | "010" : { 51 | "name" : "DPT_Value_1_Ucount", "desc" : "counter pulses", 52 | "unit" : "pulses", 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt11.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const test = require('tape'); 7 | const DPTLib = require('../../src/dptlib'); 8 | const assert = require('assert'); 9 | 10 | function dateequals(d1, d2) { 11 | var d = d1.getDate(); 12 | var m = d1.getMonth(); 13 | var y = d1.getFullYear(); 14 | return (d == d2.getDate() && m == d2.getMonth() && y == d2.getFullYear()); 15 | } 16 | 17 | test('DPT11 date conversion', function(t) { 18 | var tests = [ 19 | ['DPT11', [25, 12, 95], new Date('1995-12-25')], 20 | ['DPT11', [0x19, 0x0C, 0x0F], new Date('2015-12-25')], 21 | ['DPT11', [0x16, 0x0B, 0x10], new Date('2016-11-22')], 22 | ['DPT11', [0x1B, 0x01, 0x13], new Date('2019-01-27')], 23 | ['DPT11', [0x03, 0x02, 0x13], new Date('2019-02-03')] 24 | ] 25 | for (var i = 0; i < tests.length; i++) { 26 | var dpt = DPTLib.resolve(tests[i][0]); 27 | var buf = new Buffer(tests[i][1]); 28 | var val = tests[i][2]; 29 | 30 | // unmarshalling test (raw data to value) 31 | var converted = DPTLib.fromBuffer(buf, dpt); 32 | t.ok(dateequals(val, converted), 33 | `${tests[i][0]} fromBuffer value ${val} => ${JSON.stringify(converted)}` 34 | ); 35 | 36 | // marshalling test (value to raw data) 37 | var apdu = {}; 38 | DPTLib.populateAPDU(val, apdu, 'dpt11'); 39 | t.ok(Buffer.compare(buf, apdu.data) == 0, 40 | `${tests[i][0]} formatAPDU value ${val} => ${JSON.stringify(converted)}` 41 | ); 42 | } 43 | t.end() 44 | }) 45 | -------------------------------------------------------------------------------- /manualtest/test-writestorm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2017 Elias Karakoulakis 4 | */ 5 | Error.stackTraceLimit = Infinity; 6 | var knx = require('knx'); 7 | 8 | if (process.argv.length < 2) { 9 | console.log('usage: %s <0/1> (off/on) to write to a set of binary switches', process.argv[1]); 10 | process.exit(1); 11 | } 12 | 13 | function setupSwitch(groupaddress, statusga) { 14 | var sw = new knx.Devices.BinarySwitch({ga: groupaddress, status_ga: statusga}, connection); 15 | sw.on('change', (oldvalue, newvalue, ga) => { 16 | console.log(" %s: **** %s current value: %j", Date.now(), ga, newvalue); 17 | }); 18 | return sw; 19 | } 20 | 21 | var connection = knx.Connection({ 22 | //debug: true, 23 | //minimumDelay: 10, 24 | handlers: { 25 | connected: function() { 26 | console.log('===========\nConnected! %s \n===========', Date.now()); 27 | var v = parseInt(process.argv[2]); 28 | console.log('---- Writing %d ---', v); 29 | setupSwitch('1/1/0', '1/1/100').write(v); 30 | setupSwitch('1/1/1', '1/1/101').write(v); 31 | setupSwitch('1/1/2', '1/1/102').write(v); 32 | setupSwitch('1/1/3', '1/1/103').write(v); 33 | setupSwitch('1/1/4', '1/1/104').write(v); 34 | setupSwitch('1/1/5', '1/1/105').write(v); 35 | setupSwitch('1/1/6', '1/1/106').write(v); 36 | setupSwitch('1/1/7', '1/1/107').write(v); 37 | setupSwitch('1/1/8', '1/1/108').write(v); 38 | }, 39 | error: function( errmsg ) { 40 | console.error(errmsg); 41 | } 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt19.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const test = require('tape'); 7 | const DPTLib = require('../../src/dptlib'); 8 | 9 | test('DPT19 datetime conversion', function(t) { 10 | 11 | var tests = ['1995-12-17T03:24:00', '1996-07-17T03:24:00']; 12 | 13 | Object.keys(tests).forEach(function(key) { 14 | var date = new Date(tests[key]); 15 | date.setMilliseconds(0); 16 | 17 | var day = (date.getDay() === 0) ? 7 : date.getDay(); 18 | var buffer = new Buffer([ 19 | date.getFullYear() - 1900, // year with offset 1900 20 | date.getMonth() + 1, // month from 1 - 12 21 | date.getDate(), // day of month from 1 - 31 22 | (day << 5) + date.getHours(), // 3 bits: day of week (1-7), 5 bits: hour 23 | date.getMinutes(), 24 | date.getSeconds(), 25 | 0, 26 | 0 27 | ]); 28 | 29 | var name = 'DPT19'; 30 | var dpt = DPTLib.resolve(name); 31 | 32 | // unmarshalling test (raw data to value) 33 | var converted = DPTLib.fromBuffer(buffer, dpt); 34 | t.equal(date.getTime(), converted.getTime(), 35 | `${name} fromBuffer value ${JSON.stringify(buffer)} => ${converted}` 36 | ); 37 | 38 | // marshalling test (value to raw data) 39 | var apdu = {}; 40 | DPTLib.populateAPDU(date, apdu, name); 41 | t.ok(Buffer.compare(buffer, apdu.data) === 0, 42 | `${name} formatAPDU value ${date} => ${JSON.stringify(apdu)}` 43 | ); 44 | }); 45 | 46 | t.end(); 47 | }); 48 | -------------------------------------------------------------------------------- /src/devices/BinarySwitch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a pure Javascript library for KNX 3 | * (C) 2016 Elias Karakoulakis 4 | */ 5 | 6 | const Datapoint = require('../Datapoint'); 7 | const Log = require('../KnxLog'); 8 | 9 | class BinarySwitch { 10 | constructor(options, conn) { 11 | if (!options || !options.ga) throw 'must supply at least { ga }!'; 12 | 13 | this.control_ga = options.ga; 14 | this.status_ga = options.status_ga; 15 | if (conn) this.bind(conn); 16 | this.log = Log.get(); 17 | } 18 | bind(conn) { 19 | if (!conn) this.log.warn('must supply a valid KNX connection to bind to'); 20 | this.conn = conn; 21 | this.control = new Datapoint({ ga: this.control_ga }, conn); 22 | if (this.status_ga) 23 | this.status = new Datapoint({ ga: this.status_ga }, conn); 24 | } 25 | // EventEmitter proxy for status ga (if its set), otherwise proxy control ga 26 | on(...args) { 27 | const tgt = this.status_ga ? this.status : this.control; 28 | try { 29 | tgt.on(...args); 30 | } catch (err) { 31 | this.log.error(err); 32 | } 33 | } 34 | switchOn() { 35 | if (!this.conn) 36 | this.log.warn('must supply a valid KNX connection to bind to'); 37 | this.control.write(1); 38 | } 39 | switchOff() { 40 | if (!this.conn) 41 | this.log.warn('must supply a valid KNX connection to bind to'); 42 | this.control.write(0); 43 | } 44 | write(v) { 45 | if (!this.conn) 46 | this.log.warn('must supply a valid KNX connection to bind to'); 47 | this.control.write(v); 48 | } 49 | } 50 | 51 | module.exports = BinarySwitch; 52 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt237.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | const commontest = require('./commontest') 6 | 7 | commontest.do('DPT237', [ 8 | { apdu_data: [0x00, 0x00], jsval: {address: 0, addresstype: 0, 9 | readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, 10 | { apdu_data: [0x00, 0x21], jsval: {address: 1, addresstype: 1, 11 | readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, 12 | { apdu_data: [0x00, 0x63], jsval: {address: 3, addresstype: 1, 13 | readresponse: 1, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, 14 | { apdu_data: [0x01, 0x05], jsval: {address: 5, addresstype: 0, 15 | readresponse: 0, lampfailure: 0, ballastfailure: 1, convertorerror: 0}}, 16 | { apdu_data: [0x02, 0x08], jsval: {address: 8, addresstype: 0, 17 | readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 1}} 18 | ]); 19 | 20 | commontest.do('DPT237.600', [ 21 | { apdu_data: [0x00, 0x01], jsval: {address: 1, addresstype: 0, 22 | readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, 23 | { apdu_data: [0x00, 0x05], jsval: {address: 5, addresstype: 0, 24 | readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, 25 | { apdu_data: [0x00, 0x06], jsval: {address: 6, addresstype: 0, 26 | readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}}, 27 | { apdu_data: [0x00, 0x08], jsval: {address: 8, addresstype: 0, 28 | readresponse: 0, lampfailure: 0, ballastfailure: 0, convertorerror: 0}} 29 | ]); 30 | -------------------------------------------------------------------------------- /manualtest/test-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2017 Elias Karakoulakis 4 | */ 5 | 6 | var knx = require('knx'); 7 | 8 | if (process.argv.length < 3) { 9 | console.log('usage: %s to toggle a light on & off', 10 | process.argv[1]); 11 | process.exit(1); 12 | } 13 | var connection = knx.Connection({ 14 | debug: true, 15 | handlers: { 16 | connected: function() { 17 | console.log('----------'); 18 | console.log('Connected!'); 19 | console.log('----------'); 20 | // define a datapoint: 21 | var dp = new knx.Datapoint({ 22 | ga: process.argv[2], 23 | dpt: 'DPT1.001' 24 | }, connection); 25 | if (process.argv[3]) { 26 | var status_ga = new knx.Datapoint({ 27 | ga: process.argv[3], 28 | dpt: 'DPT1.001' 29 | }, connection); 30 | status_ga.on('change', function(oldvalue, newvalue) { 31 | console.log("**** Light %s changed from: %j to: %j", 32 | process.argv[2], oldvalue, newvalue); 33 | }); 34 | } 35 | // Now send off a couple of requests: 36 | console.log('\n\n\n'); 37 | console.log('PRESS ANY KEY TO TOGGLE %s AND "q" TO QUIT.', process.argv[2]); 38 | console.log('\n\n\n'); 39 | var dpVal = false; 40 | process.stdin.setRawMode(true); 41 | process.stdin.resume(); 42 | process.stdin.on('data', (data) => { 43 | console.log(JSON.stringify(data)); 44 | if (data[0] === 113) { 45 | process.exit(0); 46 | return; 47 | } 48 | dpVal = !dpVal; 49 | console.log("Sending " + dpVal); 50 | dp.write(dpVal); 51 | }); 52 | } 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /typescript-sample/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-unsafe-finally": true, 17 | "no-var-keyword": true, 18 | "one-line": [ 19 | true, 20 | "check-open-brace", 21 | "check-whitespace" 22 | ], 23 | "quotemark": [ 24 | true, 25 | "single" 26 | ], 27 | "semicolon": [ 28 | true, 29 | "never" 30 | ], 31 | "triple-equals": [ 32 | true, 33 | "allow-null-check" 34 | ], 35 | "typedef-whitespace": [ 36 | true, 37 | { 38 | "call-signature": "nospace", 39 | "index-signature": "nospace", 40 | "parameter": "nospace", 41 | "property-declaration": "nospace", 42 | "variable-declaration": "nospace" 43 | } 44 | ], 45 | "variable-name": [ 46 | true, 47 | "ban-keywords" 48 | ], 49 | "whitespace": [ 50 | true, 51 | "check-branch", 52 | "check-decl", 53 | "check-operator", 54 | "check-separator", 55 | "check-type" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /src/dptlib/dpt238.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT238: 1-byte unsigned value 10 | // 11 | // DPT5 is the only (AFAIK) DPT with scalar datatypes (5.001 and 5.003) 12 | exports.formatAPDU = (value) => { 13 | const apdu_data = Buffer.from([value]); 14 | log.trace( 15 | 'dpt238.js : input value = ' + value + ' apdu_data = ' + apdu_data 16 | ); 17 | return apdu_data; 18 | }; 19 | 20 | exports.fromBuffer = (buf) => buf[0]; 21 | 22 | exports.basetype = { 23 | bitlength: 8, 24 | range: [,], 25 | valuetype: 'basic', 26 | desc: '1-byte', 27 | }; 28 | 29 | exports.subtypes = { 30 | // 20.102 HVAC mode 31 | 102: { 32 | name: 'HVAC_Mode', 33 | desc: '', 34 | unit: '', 35 | scalar_range: [,], 36 | range: [,], 37 | }, 38 | 39 | // 5.003 angle (degrees 0=0, ff=360) 40 | '003': { 41 | name: 'DPT_Angle', 42 | desc: 'angle degrees', 43 | unit: '°', 44 | scalar_range: [0, 360], 45 | }, 46 | 47 | // 5.004 percentage (0..255%) 48 | '004': { 49 | name: 'DPT_Percent_U8', 50 | desc: 'percent', 51 | unit: '%', 52 | }, 53 | 54 | // 5.005 ratio (0..255) 55 | '005': { 56 | name: 'DPT_DecimalFactor', 57 | desc: 'ratio', 58 | unit: 'ratio', 59 | }, 60 | 61 | // 5.006 tariff (0..255) 62 | '006': { 63 | name: 'DPT_Tariff', 64 | desc: 'tariff', 65 | unit: 'tariff', 66 | }, 67 | 68 | // 5.010 counter pulses (0..255) 69 | '010': { 70 | name: 'DPT_Value_1_Ucount', 71 | desc: 'counter pulses', 72 | unit: 'pulses', 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /test/wiredtests/test-logotimer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const knx = require('../..'); 7 | const test = require('tape'); 8 | const util = require('util'); 9 | const options = require('./wiredtest-options.js'); 10 | 11 | /* 12 | ========== ================== 13 | this is a WIRED test and requires a real KNX IP router on the LAN 14 | ========== ================== 15 | to run all tests: $ WIREDTEST=1 npm test 16 | to run one test : $ WIREDTEST=1 node test/wiredtests/.js 17 | */ 18 | if (process.env.hasOwnProperty('WIREDTEST')) { 19 | test('KNX wired test - control a DPT9 timer', function(t) { 20 | var connection = new knx.Connection( { 21 | //debug: true, 22 | handlers: { 23 | connected: () => { 24 | var timer_control = new knx.Datapoint({ga: options.dpt9_timer_control_ga, dpt: 'DPT9.001', autoread: true}, connection); 25 | var timer_status = new knx.Datapoint({ga: options.dpt9_timer_status_ga, dpt: 'DPT9.001', autoread: true}, connection); 26 | timer_control.on('change', function(oldvalue, newvalue) { 27 | t.pass(util.format("**** Timer control changed from: %j to: %j", oldvalue, newvalue)); 28 | }); 29 | timer_status.read(function(src, response) { 30 | t.pass(util.format("**** Timer status response: %j", response)); 31 | t.end(); 32 | process.exit(0); 33 | }); 34 | timer_control.write(12); 35 | } 36 | } 37 | }); 38 | }); 39 | 40 | setTimeout(function () { 41 | process.exit(1); 42 | }, 1000); 43 | } 44 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const test = require('tape'); 7 | const DPTLib = require('../../src/dptlib'); 8 | const assert = require('assert'); 9 | 10 | test('resolve', function(t) { 11 | t.throws(() => { 12 | DPTLib.resolve('invalid input'); 13 | }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); 14 | 15 | t.throws(() => { 16 | DPTLib.resolve({dpt: 9}); 17 | }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); 18 | 19 | t.throws(() => { 20 | DPTLib.resolve([9,9]); 21 | }, /Invalid DPT format: .*/, 'Invalid format of a DPT'); 22 | 23 | t.throws(() => { 24 | DPTLib.resolve('29.010'); 25 | }, /Unsupported DPT: .*/, 'Unsupported/unknown DPT'); 26 | 27 | t.throws(() => { 28 | DPTLib.resolve(29); 29 | }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT'); 30 | 31 | t.throws(() => { 32 | DPTLib.resolve([29]); 33 | }, /Unsupported DPT: .*/, 'Unsupported/unknown Int DPT'); 34 | 35 | var d0 = DPTLib.resolve(1) 36 | t.equal(d0.id, 'DPT1') 37 | t.equal(d0.subtypeid, undefined) 38 | 39 | var d1 = DPTLib.resolve('DPT9') 40 | t.equal(d1.id, 'DPT9') 41 | t.equal(d1.subtypeid, undefined) 42 | 43 | var d2 = DPTLib.resolve('DPT1.002') 44 | t.equal(d2.id, 'DPT1') 45 | t.equal(d2.subtypeid, '002') 46 | 47 | var d3 = DPTLib.resolve('DPT1.001') 48 | t.equal(d3.id, 'DPT1') 49 | t.equal(d3.subtypeid, '001') 50 | 51 | // Check that dpts are not destroyed by subsequent calls to resolve 52 | t.equal(d2.id, 'DPT1') 53 | t.equal(d2.subtypeid, '002') 54 | 55 | var d4 = DPTLib.resolve('1.002') 56 | t.equal(d4.id, 'DPT1') 57 | t.equal(d4.subtypeid, '002') 58 | 59 | t.end() 60 | }) 61 | -------------------------------------------------------------------------------- /test/wiredtests/test-read.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const knx = require('../..'); 7 | const test = require('tape'); 8 | const util = require('util'); 9 | const options = require('./wiredtest-options.js'); 10 | 11 | /* 12 | ========== ================== 13 | this is a WIRED test and requires a real KNX IP router on the LAN 14 | ========== ================== 15 | to run all tests: $ WIREDTEST=1 npm test 16 | to run one test : $ WIREDTEST=1 node test/wiredtests/.js 17 | */ 18 | if (process.env.hasOwnProperty('WIREDTEST')) { 19 | test('KNX wired test - read a temperature', function(t) { 20 | var connection = new knx.Connection({ 21 | debug: true, 22 | physAddr: options.physAddr, 23 | handlers: { 24 | connected: function() { 25 | // just define a temperature GA that should respond to a a GroupValue_Read request 26 | var temperature_in = new knx.Datapoint({ 27 | ga: options.dpt9_temperature_status_ga, 28 | dpt: 'DPT9.001' 29 | }, connection); 30 | temperature_in.read(function(src, response) { 31 | console.log("KNX response from %s: %j", src, response); 32 | t.pass(util.format('read temperature: %s', response)); 33 | t.end(); 34 | process.exit(0); 35 | }); 36 | }, 37 | error: function(connstatus) { 38 | console.log("%s **** ERROR: %j", 39 | new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), 40 | connstatus); 41 | process.exit(1); 42 | } 43 | } 44 | }); 45 | }); 46 | 47 | setTimeout(function() { 48 | console.log('Exiting ...'); 49 | process.exit(2); 50 | }, 1500); 51 | } 52 | -------------------------------------------------------------------------------- /test/wiredtests/test-connect-tunnel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | Error.stackTraceLimit = Infinity; 7 | 8 | const knx = require('../..'); 9 | const address = require('../../src/Address.js'); 10 | const assert = require('assert'); 11 | const test = require('tape'); 12 | 13 | const options = require('./wiredtest-options.js'); 14 | 15 | /* 16 | ========== ================== 17 | this is a WIRED test and requires a real KNX IP router on the LAN 18 | ========== ================== 19 | to run all tests: $ WIREDTEST=1 npm test 20 | to run one test : $ WIREDTEST=1 node test/wiredtests/.js 21 | */ 22 | if (process.env.hasOwnProperty('WIREDTEST')) { 23 | // 24 | test('KNX connect tunneling', function(t) { 25 | var connection = knx.Connection({ 26 | // set up your KNX IP router's IP address (not multicast!) 27 | // for getting into tunnelling mode 28 | ipAddr: options.ipAddr, 29 | physAddr: options.physAddr, 30 | debug: true, 31 | handlers: { 32 | connected: function() { 33 | console.log('----------'); 34 | console.log('Connected!'); 35 | console.log('----------'); 36 | t.pass('connected in TUNNELING mode'); 37 | this.Disconnect(); 38 | }, 39 | disconnected: function() { 40 | t.pass('disconnected in TUNNELING mode'); 41 | t.end(); 42 | process.exit(0); 43 | }, 44 | error: function(connstatus) { 45 | t.fail('error connecting: '+connstatus); 46 | t.end(); 47 | process.exit(1); 48 | } 49 | } 50 | }); 51 | }); 52 | 53 | setTimeout(function() { 54 | console.log('Exiting with timeout...'); 55 | process.exit(2); 56 | }, 1000); 57 | } 58 | -------------------------------------------------------------------------------- /src/dptlib/dpt13.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT13: 4-byte signed value 8 | // 9 | 10 | // DPT13 base type info 11 | exports.basetype = { 12 | "bitlength" : 32, 13 | "signedness": "signed", 14 | "valuetype" : "basic", 15 | "desc" : "4-byte signed value", 16 | "range" : [-Math.pow(2, 31), Math.pow(2, 31)-1] 17 | } 18 | 19 | // DPT13 subtypes 20 | exports.subtypes = { 21 | // 13.001 counter pulses (signed) 22 | "001" : { 23 | "name" : "DPT_Value_4_Count", "desc" : "counter pulses (signed)", 24 | "unit" : "pulses" 25 | }, 26 | 27 | "002" : { 28 | "name" : "DPT_Value_Activation_Energy", "desc" : "activation energy (J/mol)" , 29 | "unit" : "J/mol" 30 | }, 31 | 32 | // 13.010 active energy (Wh) 33 | "010" : { 34 | "name" : "DPT_ActiveEnergy", "desc" : "active energy (Wh)", 35 | "unit" : "Wh" 36 | }, 37 | 38 | // 13.011 apparent energy (VAh) 39 | "011" : { 40 | "name" : "DPT_ApparantEnergy", "desc" : "apparent energy (VAh)", 41 | "unit" : "VAh" 42 | }, 43 | 44 | // 13.012 reactive energy (VARh) 45 | "012" : { 46 | "name" : "DPT_ReactiveEnergy", "desc" : "reactive energy (VARh)", 47 | "unit" : "VARh" 48 | }, 49 | 50 | // 13.013 active energy (KWh) 51 | "013" : { 52 | "name" : "DPT_ActiveEnergy_kWh", "desc" : "active energy (kWh)", 53 | "unit" : "kWh" 54 | }, 55 | 56 | // 13.014 apparent energy (kVAh) 57 | "014" : { 58 | "name" : "DPT_ApparantEnergy_kVAh", "desc" : "apparent energy (kVAh)", 59 | "unit" : "VAh" 60 | }, 61 | 62 | // 13.015 reactive energy (kVARh) 63 | "015" : { 64 | "name" : "DPT_ReactiveEnergy_kVARh", "desc" : "reactive energy (kVARh)", 65 | "unit" : "kVARh" 66 | }, 67 | 68 | // 13.100 time lag(s) 69 | "100" : { 70 | "name" : "DPT_LongDeltaTimeSec", "desc" : "time lag(s)", 71 | "unit" : "s" 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /src/dptlib/dpt8.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT8.*: 2-byte signed value 8 | // 9 | 10 | // DPT8 basetype info 11 | exports.basetype = { 12 | "bitlength" : 16, 13 | "signedness": "signed", 14 | "valuetype" : "basic", 15 | "range" : [-32768, 32767], 16 | "desc" : "16-bit signed value" 17 | } 18 | 19 | // DPT8 subtypes info 20 | exports.subtypes = { 21 | // 8.001 pulses difference 22 | "001" : { 23 | "name" : "DPT_Value_2_Count", 24 | "desc" : "pulses", 25 | "unit" : "pulses" 26 | }, 27 | 28 | // 8.002 time lag (ms) 29 | "002" : { 30 | "name" : "DPT_DeltaTimeMsec", 31 | "desc" : "time lag(ms)", 32 | "unit" : "milliseconds" 33 | }, 34 | 35 | // 8.003 time lag (10ms) 36 | "003" : { 37 | "name" : "DPT_DeltaTime10Msec", 38 | "desc" : "time lag(10ms)", 39 | "unit" : "centiseconds" 40 | }, 41 | 42 | // 8.004 time lag (100ms) 43 | "004" : { 44 | "name" : "DPT_DeltaTime100Msec", 45 | "desc" : "time lag(100ms)", 46 | "unit" : "deciseconds" 47 | }, 48 | 49 | // 8.005 time lag (sec) 50 | "005" : { 51 | "name" : "DPT_DeltaTimeSec", 52 | "desc" : "time lag(s)", 53 | "unit" : "seconds" 54 | }, 55 | 56 | // 8.006 time lag (min) 57 | "006" : { 58 | "name" : "DPT_DeltaTimeMin", 59 | "desc" : "time lag(min)", 60 | "unit" : "minutes" 61 | }, 62 | 63 | // 8.007 time lag (hour) 64 | "007" : { 65 | "name" : "DPT_DeltaTimeHrs", 66 | "desc" : "time lag(hrs)", 67 | "unit" : "hours" 68 | }, 69 | 70 | // 8.010 percentage difference (%) 71 | "010" : { 72 | "name" : "DPT_Percent_V16", 73 | "desc" : "percentage difference", 74 | "unit" : "%" 75 | }, 76 | 77 | // 8.011 rotation angle (deg) 78 | "011" : { 79 | "name" : "DPT_RotationAngle", 80 | "desc" : "angle(degrees)", 81 | "unit" : "°" 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /test/dptlib/test-dpt10.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const test = require('tape'); 7 | const DPTLib = require('../../src/dptlib'); 8 | const assert = require('assert'); 9 | 10 | function timecompare(date1, sign, date2) { 11 | var dow1 = date1.getDay(); 12 | var hour1 = date1.getHours(); 13 | var min1 = date1.getMinutes(); 14 | var sec1 = date1.getSeconds(); 15 | var dow2 = date2.getDay(); 16 | var hour2 = date2.getHours(); 17 | var min2 = date2.getMinutes(); 18 | var sec2 = date2.getSeconds(); 19 | if (sign === '===') { 20 | if (dow1 == dow2 && hour1 === hour2 && min1 === min2 && sec1 === sec2) return true; 21 | else return false; 22 | } else if (sign === '>') { 23 | if (dow1 > dow2) return true; 24 | else if (dow1 == dow2 && hour1 > hour2) return true; 25 | else if (dow1 == dow2 && hour1 === hour2 && min1 > min2) return true; 26 | else if (dow1 == dow2 && hour1 === hour2 && min1 === min2 && sec1 > sec2) return true; 27 | else return false; 28 | } 29 | } 30 | 31 | test('DPT10 time conversion', function(t) { 32 | 33 | var tests = [ 34 | ['DPT10', [(1<<5)+23, 15, 30], new Date('July 1, 2019 23:15:30')], // Monday 35 | ['DPT10', [(3<<5)+14, 55, 11], new Date('July 10, 2019 14:55:11')],// Wednesday 36 | ['DPT10', [(7<<5)+23, 15, 30], new Date('July 7, 2019 23:15:30')] // Sunday 37 | ]; 38 | for (var i = 0; i < tests.length; i++) { 39 | var dpt = DPTLib.resolve(tests[i][0]); 40 | var buf = new Buffer(tests[i][1]); 41 | var val = tests[i][2]; 42 | 43 | // unmarshalling test (raw data to value) 44 | var converted = DPTLib.fromBuffer(buf, dpt); 45 | t.ok(timecompare(converted, '===', val) , 46 | `${tests[i][0]} fromBuffer value ${buf.toString('hex')} => expected ${val}, got ${converted}`); 47 | 48 | // marshalling test (value to raw data) 49 | var apdu = {}; 50 | DPTLib.populateAPDU(val, apdu, 'dpt10'); 51 | t.ok(Buffer.compare(buf, apdu.data) == 0, 52 | `${tests[i][0]} formatAPDU value ${val} => expected ${buf.toString('hex')}, got ${apdu.data.toString('hex')}`); 53 | } 54 | t.end() 55 | }) 56 | -------------------------------------------------------------------------------- /src/IpRoutingConnection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const util = require('util'); 7 | const dgram = require('dgram'); 8 | const KnxLog = require('./KnxLog.js'); 9 | /** 10 | Initializes a new KNX routing connection with provided values. Make 11 | sure the local system allows UDP messages to the multicast group. 12 | **/ 13 | function IpRoutingConnection(instance) { 14 | const log = KnxLog.get(); 15 | 16 | instance.BindSocket = function (cb) { 17 | const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true }); 18 | udpSocket.on('listening', () => { 19 | log.debug( 20 | util.format( 21 | 'IpRoutingConnection %s:%d, adding membership for %s', 22 | instance.localAddress, 23 | udpSocket.address().port, 24 | this.remoteEndpoint.addr 25 | ) 26 | ); 27 | try { 28 | this.socket.addMembership( 29 | this.remoteEndpoint.addr, 30 | instance.localAddress 31 | ); 32 | } catch (err) { 33 | log.warn('IPRouting connection: cannot add membership (%s)', err); 34 | } 35 | }); 36 | // ROUTING multicast connections need to bind to the default port, 3671 37 | udpSocket.bind(3671, () => cb && cb(udpSocket)); 38 | return udpSocket; 39 | }; 40 | 41 | // 42 | /// Start the connection 43 | /// 44 | instance.Connect = function () { 45 | this.localAddress = this.getLocalAddress(); 46 | this.socket = this.BindSocket((socket) => { 47 | socket.on('error', (errmsg) => 48 | log.debug(util.format('Socket error: %j', errmsg)) 49 | ); 50 | socket.on('message', (msg, rinfo, callback) => { 51 | log.debug( 52 | 'Inbound multicast message from ' + 53 | rinfo.address + 54 | ': ' + 55 | msg.toString('hex') 56 | ); 57 | this.onUdpSocketMessage(msg, rinfo, callback); 58 | }); 59 | // start connection sequence 60 | this.transition('connecting'); 61 | }); 62 | return this; 63 | }; 64 | 65 | return instance; 66 | } 67 | 68 | module.exports = IpRoutingConnection; 69 | -------------------------------------------------------------------------------- /src/dptlib/dpt11.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | const util = require('util'); 8 | // 9 | // DPT11.*: date 10 | // 11 | exports.formatAPDU = (value) => { 12 | if (value == null) return log.error('cannot write null value for DPT11'); 13 | switch (typeof value) { 14 | case 'string': 15 | case 'number': 16 | value = new Date(value); 17 | break; 18 | case 'object': 19 | // this expects the month property to be zero-based (January = 0, etc.) 20 | if (value instanceof Date) break; 21 | const { year, month, day } = value; 22 | value = new Date(parseInt(year), parseInt(month), parseInt(day)); 23 | } 24 | if (isNaN(value.getDate())) 25 | return log.error( 26 | 'Must supply a numeric timestamp, Date or String object for DPT11 Date' 27 | ); 28 | 29 | const year = value.getFullYear(); 30 | return Buffer.from([ 31 | value.getDate(), 32 | value.getMonth() + 1, 33 | year - (year >= 2000 ? 2000 : 1900), 34 | ]); 35 | }; 36 | 37 | exports.fromBuffer = (buf) => { 38 | if (buf.length != 3) return log.error('Buffer should be 3 bytes long'); 39 | const day = buf[0] & 31; //0b00011111); 40 | const month = buf[1] & 15; //0b00001111); 41 | let year = buf[2] & 127; //0b01111111); 42 | year = year + (year > 89 ? 1900 : 2000); 43 | if ( 44 | day < 1 || 45 | day > 31 || 46 | month < 1 || 47 | month > 12 || 48 | year < 1990 || 49 | year > 2089 50 | ) { 51 | log.error( 52 | '%j => %d/%d/%d is not valid date according to DPT11, setting to 1990/01/01', 53 | buf, 54 | day, 55 | month, 56 | year 57 | ); 58 | //return new Date(1990, 01, 01); 59 | throw new Error('Error converting date buffer to Date object.'); 60 | } 61 | return new Date(year, month - 1, day); 62 | }; 63 | 64 | // DPT11 base type info 65 | exports.basetype = { 66 | bitlength: 24, 67 | valuetype: 'composite', 68 | desc: '3-byte date value', 69 | }; 70 | 71 | // DPT11 subtypes info 72 | exports.subtypes = { 73 | // 11.001 date 74 | '001': { 75 | name: 'DPT_Date', 76 | desc: 'Date', 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /README-events.md: -------------------------------------------------------------------------------- 1 | ## Events 2 | 3 | There's a ton of information being emitted by the library, so you can get full disclosure as to what's going on under the hood. 4 | 5 | ### Connection events 6 | 7 | ```js 8 | // Generic groupwrite event: device with 'src' physical address wrote to 'dest' group address 9 | connection.on('GroupValue_Write', function (src, dest, value) { ... }); 10 | // Generic groupread event: device with physical address 'src', is asking on the KNX 11 | // bus the current value of group address 'dest' 12 | connection.on('GroupValue_Read', function (src, dest) { ... }); 13 | // response event: device with physical address 'src', is responding to a 14 | // read request that the current value of group address 'dest' is 'value' 15 | connection.on('GroupValue_Response', function (src, dest, value) { ... }); 16 | 17 | // Specific group address event: device with 'src' physical address 18 | // .. wrote to group address 19 | connection.on('GroupValue_Write_1/2/3', function (src, value) { ... }); 20 | // .. or responded about current value 21 | connection.on('GroupValue_Response_1/2/3', function (src, value) { ... }); 22 | 23 | // there's also the generic catch-all event which passes the event type 24 | // as its 1st argument, along with all the other info 25 | connection.on('event', function (evt, src, dest, value) { ... });) 26 | // the generic catch-all event can also be used with group addresses 27 | connection.on('event_1/2/3', function (evt, src, dest, value) { ... });) 28 | ``` 29 | 30 | Here's the full list of events emitted by the connection: 31 | ``` 32 | ["GroupValue_Read", "GroupValue_Response", "GroupValue_Write", 33 | "PhysicalAddress_Write", "PhysicalAddress_Read", "PhysicalAddress_Response", 34 | "ADC_Read", "ADC_Response", "Memory_Read", "Memory_Response", "Memory_Write", 35 | "UserMemory", "DeviceDescriptor_Read", "DeviceDescriptor_Response", 36 | "Restart", "OTHER"] 37 | ``` 38 | 39 | Other connection events: 40 | ```js 41 | // an outgoing tunnelling request didn't get any acknowledgement 42 | connection.on('unacknowledged', function (datagram) { ... });) 43 | ``` 44 | 45 | ### Datapoint events 46 | 47 | ```js 48 | // a datapoint has had its value changed 49 | datapoint.on('change', function(oldvalue, newvalue){...}); 50 | ``` 51 | -------------------------------------------------------------------------------- /src/dptlib/dpt21.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT21: 1-byte status 10 | // 11 | // 001 12 | // - OutofService b0 13 | // - Overridden b1 14 | // - Inalarm b2 15 | // - AlarmUnAck b3 16 | // - reseverd b4-7 17 | 18 | // FIXME: help needed 19 | exports.formatAPDU = function(value) { 20 | if (value == null) return log.error('DPT21: cannot write null value'); 21 | log.debug('./knx/src/dpt21.js : input value = ' + value); 22 | 23 | //var apdu_data = new Buffer(1); 24 | //apdu_data[0] = value; 25 | if ( typeof value === 'object' ) 26 | return Buffer.from([(value.outofservice) + 27 | (value.fault << 1) + 28 | (value.overridden << 2) + 29 | (value.inalarm << 3) + 30 | (value.alarmeunack << 4) ]); 31 | 32 | log.error('DPT21: Must supply a value which is an object'); 33 | //return apdu_data; 34 | return Buffer.from([0]); 35 | } 36 | 37 | exports.fromBuffer = function(buf) { 38 | if (buf.length != 1) return log.error ("Buffer should be 1 bytes long"); 39 | //if (buf.length != 1) throw "Buffer should be 1 bytes long"; 40 | log.debug(' dpt21.js fromBuffer : ' + buf); 41 | 42 | //var ret = buf.readUInt8(0); 43 | 44 | return { 45 | outofservice: (buf[0] & 0b00000001), 46 | fault: (buf[0] & 0b00000010) >> 1, 47 | overridden: (buf[0] & 0b00000100) >> 2, 48 | inalarm: (buf[0] & 0b00001000) >> 3, 49 | alarmunack: (buf[0] & 0b00010000) >> 4 }; 50 | //return ret; 51 | } 52 | 53 | 54 | exports.basetype = { 55 | "bitlength" : 8, 56 | "range" : [ , ], 57 | "valuetype" : "composite", 58 | "desc" : "1-byte" 59 | } 60 | 61 | exports.subtypes = { 62 | // 21.001 status - 5 bits 63 | "001" : { 64 | "name" : "DPT_StatusGen", 65 | "desc" : "General Status", 66 | "unit" : "", 67 | "scalar_range" : [ , ], 68 | "range" : [ , ] 69 | }, 70 | // 21.002 control - 3 bits 71 | "002" : { 72 | "name" : "DPT_Device_Control", 73 | "desc" : "Device Control", 74 | "unit" : "", 75 | "scalar_range" : [ , ], 76 | "range" : [ , ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/dptlib/dpt237.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT237: 2-byte unsigned value 10 | // 11 | exports.formatAPDU = function(value) { 12 | if (value == null) return log.error('DPT237: cannot write null value'); 13 | 14 | log.trace('dpt278.js : input value = ' + value); 15 | 16 | var apdu_data = new Buffer(2); 17 | 18 | //console.log("Buffer lenght: ", apdu_data.length); 19 | if ( typeof value === 'object' && 20 | value.hasOwnProperty('addresstype') && 21 | value.hasOwnProperty('address') && 22 | value.hasOwnProperty('readresponse') && 23 | value.hasOwnProperty('lampfailure') && 24 | value.hasOwnProperty('ballastfailure') && 25 | value.hasOwnProperty('convertorerror') 26 | ) { 27 | // these are reversed as [0] is high and [1] low 28 | apdu_data[1] = [(value.address & 0b00011111) + 29 | (value.addresstype << 5) + 30 | (value.readresponse << 6) + 31 | (value.lampfailure << 7)]; 32 | apdu_data[0] = [(value.ballastfailure & 0b00000001) + 33 | (value.convertorerror << 1) ]; 34 | 35 | return apdu_data; 36 | } 37 | 38 | log.error('DPT237: Must supply an value {address:[0,63] or [0,15], address type:{0,1}, ...}'); 39 | 40 | return apdu_data; 41 | } 42 | 43 | exports.fromBuffer = function(buf) { 44 | if (buf.length !== 2) return log.error('Buffer should be 2 byte long'); 45 | 46 | return { address: (buf[1] & 0b00011111), 47 | addresstype: (buf[1] & 0b00100000) >> 5, 48 | readresponse: (buf[1] & 0b01000000) >> 6, 49 | lampfailure: (buf[1] & 0b10000000) >> 7, 50 | ballastfailure: (buf[0] & 0b00000001), 51 | convertorerror: (buf[0] & 0b00000010) >> 1 }; 52 | } 53 | 54 | 55 | exports.basetype = { 56 | "bitlength" : 16, 57 | "range" : [ , ], 58 | "valuetype" : "composite", 59 | "desc" : "2-byte" 60 | } 61 | 62 | exports.subtypes = { 63 | // 237.600 HVAC mode 64 | "600" : { 65 | "name" : "HVAC_Mode", 66 | "desc" : "", 67 | "unit" : "", 68 | "scalar_range" : [ , ], 69 | "range" : [ , ] 70 | }, 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/dptlib/dpt3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT3.*: 4-bit dimming/blinds control 10 | // 11 | exports.formatAPDU = (value) => { 12 | if (value == null) return log.warn('DPT3: cannot write null value'); 13 | 14 | if ( 15 | typeof value == 'object' && 16 | value.hasOwnProperty('decr_incr') && 17 | value.hasOwnProperty('data') 18 | ) 19 | return Buffer.from([(value.decr_incr << 3) + (value.data & 0b00000111)]); 20 | 21 | log.error('Must supply a value object of {decr_incr, data}'); 22 | // FIXME: should this return zero buffer when error? Or nothing? 23 | return Buffer.from([0]); 24 | }; 25 | 26 | exports.fromBuffer = (buf) => { 27 | if (buf.length != 1) return log.error('DPT3: Buffer should be 1 byte long'); 28 | 29 | return { 30 | decr_incr: (buf[0] & 0b00001000) >> 3, 31 | data: buf[0] & 0b00000111, 32 | }; 33 | }; 34 | 35 | exports.basetype = { 36 | bitlength: 4, 37 | valuetype: 'composite', 38 | desc: '4-bit relative dimming control', 39 | }; 40 | 41 | exports.subtypes = { 42 | // 3.007 dimming control 43 | '007': { 44 | name: 'DPT_Control_Dimming', 45 | desc: 'dimming control', 46 | }, 47 | 48 | // 3.008 blind control 49 | '008': { 50 | name: 'DPT_Control_Blinds', 51 | desc: 'blinds control', 52 | }, 53 | }; 54 | 55 | /* 56 | 2.6.3.5 Behavior 57 | Status 58 | off dimming actuator switched off 59 | on dimming actuator switched on, constant brightness, at least 60 | minimal brightness dimming 61 | dimming actuator switched on, moving from actual value in direction of 62 | set value 63 | Events 64 | position = 0 off command 65 | position = 1 on command 66 | control = up dX command, dX more bright dimming 67 | control = down dX command, dX less bright dimming 68 | control = stop stop command 69 | value = 0 dimming value = off 70 | value = x% dimming value = x% (not zero) 71 | value_reached actual value reached set value 72 | 73 | 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 74 | the full dimming range (0 - FFh). 75 | 76 | 3.007 dimming control 77 | 3.008 blind control 78 | */ 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knx", 3 | "description": "KNXnet/IP protocol implementation for Node(>=6.x)", 4 | "version": "2.5.4", 5 | "engines": { 6 | "node": ">=6" 7 | }, 8 | "private": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ekarak/knx.git" 12 | }, 13 | "scripts": { 14 | "test": "multi-tape test/*/test-*.js" 15 | }, 16 | "license": "MIT", 17 | "author": { 18 | "name": "Elias Karakoulakis", 19 | "email": "Elias.Karakoulakis@gmail.com" 20 | }, 21 | "maintainers": [ 22 | { 23 | "name": "Elias Karakoulakis", 24 | "email": "elias.karakoulakis@gmail.com" 25 | } 26 | ], 27 | "contributors": [ 28 | { 29 | "name": "Mattias Holmlund", 30 | "email": "mattias@holmlund.se" 31 | }, 32 | { 33 | "name": "Kay Ringmann", 34 | "email": "info@punktnetzwerk.net" 35 | }, 36 | { 37 | "name": "Timo Krieger", 38 | "url": "https://bitbucket.org/timokri/" 39 | }, 40 | { 41 | "name": "Cédric Trévisan", 42 | "url": "https://bitbucket.org/ctrevisan/" 43 | }, 44 | { 45 | "name": "Timo Müller", 46 | "email": "timomueller1993@gmail.com" 47 | }, 48 | { 49 | "name": "Patrik Åkerfeldt" 50 | }, 51 | { 52 | "name": "Johannes Ritter" 53 | }, 54 | { 55 | "name": "Koen Reynaert" 56 | }, 57 | { 58 | "name": "Luca Lanziani", 59 | "url": "https://lanziani.com" 60 | }, 61 | { 62 | "name": "Stefaan Seys" 63 | }, 64 | { 65 | "name": "Matthias Fechner" 66 | }, 67 | { 68 | "name": "Christian Duernberger" 69 | }, 70 | { 71 | "name": "Peter Körner", 72 | "email": "pkoerner@seibert-media.net" 73 | }, 74 | { 75 | "name": "Tomasz Kulpa" 76 | }, 77 | { 78 | "name": "Greg Brougham" 79 | }, 80 | { 81 | "name": "Daniel Lando", 82 | "email": "daniel.sorridi@gmail.com" 83 | } 84 | ], 85 | "keywords": [ 86 | "knx", 87 | "smart home", 88 | "building automation" 89 | ], 90 | "dependencies": { 91 | "@types/node": "^6.14.4", 92 | "binary-parser": "1.1.5", 93 | "binary-protocol": "0.0.0", 94 | "ipaddr.js": "1.2.0", 95 | "log-driver": "1.2.7", 96 | "machina": "^4.0.2" 97 | }, 98 | "devDependencies": { 99 | "multi-tape": "^1.2.1", 100 | "tape": "^4.10.1" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/knxproto/test-address.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const address = require('../../src/Address.js'); 7 | const assert = require('assert'); 8 | const test = require('tape'); 9 | 10 | // 11 | test('KNX physical address test', function(t) { 12 | var tests = { 13 | "0.0.0": new Buffer([0, 0]), 14 | "0.0.10": new Buffer([0, 10]), 15 | "0.0.255": new Buffer([0, 255]), 16 | "0.1.0": new Buffer([1, 0]), 17 | "1.0.0": new Buffer([16, 0]), 18 | "15.14.0": new Buffer([254, 0]), 19 | "15.15.0": new Buffer([255, 0]), 20 | }; 21 | Object.keys(tests).forEach((key, idx) => { 22 | var buf = tests[key]; 23 | var encoded = address.parse(key, address.TYPE.PHYSICAL); 24 | t.ok(Buffer.compare(encoded, buf) == 0, `Marshaled KNX physical address ${key}: encoded=${encoded.toString()} buf=${buf.toString()}`); 25 | var decoded = address.toString(encoded, address.TYPE.PHYSICAL); 26 | t.ok(decoded == key, `${key}: unmarshaled KNX physical address`); 27 | }); 28 | // test invalid physical addresses 29 | var invalid = ["0.0.", "0.0.256", "123122312312312", "16.0.0", "15.17.13"]; 30 | for (var i in invalid) { 31 | var key = invalid[i]; 32 | t.throws(() => { 33 | address.parse(key); 34 | }, null, `invalid KNX physical address ${key}`); 35 | } 36 | t.end(); 37 | }); 38 | 39 | // 40 | test('KNX group address test', function(t) { 41 | var tests = { 42 | "0/0/0": new Buffer([0, 0]), 43 | "0/0/10": new Buffer([0, 10]), 44 | "0/0/255": new Buffer([0, 255]), 45 | "0/1/0": new Buffer([1, 0]), 46 | "1/0/0": new Buffer([8, 0]), 47 | "1/7/0": new Buffer([15, 0]), 48 | "31/6/0": new Buffer([254, 0]), 49 | "31/7/0": new Buffer([255, 0]), 50 | }; 51 | Object.keys(tests).forEach((key, idx) => { 52 | var buf = tests[key]; 53 | var encoded = address.parse(key, address.TYPE.GROUP); 54 | t.ok(Buffer.compare(encoded, buf) == 0, `Marshaled KNX group address ${key}: encoded=${encoded.toString('hex')} buf=${buf.toString('hex')}`); 55 | var decoded = address.toString(encoded, address.TYPE.GROUP); 56 | t.ok(decoded == key, `${key}: unmarshaled KNX group address`); 57 | }); 58 | 59 | var invalid = ["0/0/", "0/0/256", "123122312312312", "16/0/0", "15/17/13"]; 60 | for (var i in invalid) { 61 | var key = invalid[i]; 62 | t.throws(() => { 63 | address.parse(key); 64 | }, null, `invalid KNX group address ${key}`); 65 | } 66 | t.end(); 67 | }); 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## KNXnet/IP for Node.JS 2 | 3 | [![CI](https://github.com/ekarak/knx/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ekarak/knx/actions/workflows/ci.yml) 4 | 5 | **New:** [Join the Gitter.im chatroom!](https://gitter.im/knx-js/Lobby) 6 | 7 | A feature-complete [KNXnet/IP protocol stack](https://www.knx.org/en-us/knx/technology/developing/devices/ip-devices/index.php) in pure Javascript, capable of talking multicast (routing) and unicast (tunneling). Adding KNX to your Node.JS applications is now finally easy as pie. 8 | 9 | * Wide DPT (datapoint type) support (DPT1 - DPT20 supported) 10 | * Extensible Device support (binary lights, dimmers, ...) 11 | * You won't need to install a specialised eibd daemon with its arcane dependencies and most importantly, 12 | * If you got an IP router and a network that supports IP multicast, you can start talking to KNX within seconds! 13 | 14 | ## Installation 15 | 16 | Make sure your machine has Node.JS (version 4.x or greater) and do: 17 | 18 | `npm install knx` 19 | 20 | ## Usage 21 | 22 | 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: 23 | 24 | ```js 25 | var knx = require('knx'); 26 | var connection = knx.Connection({ 27 | handlers: { 28 | connected: function() { 29 | console.log('Connected!'); 30 | }, 31 | event: function (evt, src, dest, value) { 32 | console.log("%s **** KNX EVENT: %j, src: %j, dest: %j, value: %j", 33 | new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''), 34 | evt, src, dest, value); 35 | } 36 | } 37 | }); 38 | ``` 39 | 40 | Ahhh, KNX telegrams, what a joy: 41 | 42 | ``` 43 | > 2016-09-24 05:34:07 **** KNX EVENT: "GroupValue_Write", src: "1.1.100", dest: "5/0/8", value: 1 44 | 2016-09-24 05:34:09 **** KNX EVENT: "GroupValue_Write", src: "1.1.100", dest: "5/1/15", value: 0 45 | 2016-09-24 05:34:09 **** KNX EVENT: "GroupValue_Write", src: "1.1.100", dest: "5/0/8", value: 0 46 | 2016-09-24 05:34:17 **** KNX EVENT: "GroupValue_Write", src: "1.1.100", dest: "5/1/15", value: 0 47 | 2016-09-24 05:34:17 **** KNX EVENT: "GroupValue_Write", src: "1.1.100", dest: "5/0/8", value: 1 48 | ``` 49 | 50 | ## Development documentation 51 | 52 | - [Basic API usage](../master/README-API.md) 53 | - [List of supported datapoints](../master/README-datapoints.md) 54 | - [List of supported events](../master/README-events.md) 55 | - [eibd/knxd compatibility](../master/README-knxd.md) 56 | - [On resilience](../master/README-resilience.md) 57 | -------------------------------------------------------------------------------- /test/dptlib/commontest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const test = require('tape'); 7 | const DPTLib = require('../../src/dptlib'); 8 | const assert = require('assert'); 9 | 10 | /* common DPT unit test. Tries to 11 | - 1. marshal a JS value into an apdu.data (Buffer) and compare it output to the expected value 12 | - 2. unmarshal the produced APDU from step 1 and compare it to the initial JS value 13 | - 3. unmarshal the expected APDU from the test definition and compare it to the initial JS value 14 | */ 15 | 16 | // marshalling test (JS value to APDU) 17 | function marshalTest(t, dptid, jsval, apdu) { 18 | var marshalled = {}; 19 | DPTLib.populateAPDU(jsval, marshalled, dptid); 20 | // console.log('%j --> %j', apdu.constructor.name, marshalled.data) 21 | t.deepEqual(marshalled.data, apdu, 22 | `${dptid}.populateAPDU(${jsval}:${typeof jsval}) should be marshalled as \"0x${apdu.toString('hex')}\", got: \"0x${marshalled.data.toString('hex')}\"` 23 | ); 24 | return marshalled.data; 25 | }; 26 | 27 | function unmarshalTest(t, dptid, jsval, data) { 28 | var dpt = DPTLib.resolve(dptid); 29 | var unmarshalled = DPTLib.fromBuffer(data, dpt); 30 | //console.log('%s: %j --> %j', dpt.id, rhs, converted); 31 | var msg = `${dptid}.fromBuffer(${JSON.stringify(data)}) should unmarshall to ${JSON.stringify(jsval)}, got: ${JSON.stringify(unmarshalled)}` 32 | switch (typeof jsval) { 33 | case 'object': 34 | t.deepEqual(unmarshalled, jsval, msg); 35 | break; 36 | case 'number': 37 | t.equal(unmarshalled, jsval, msg); 38 | break; 39 | default: 40 | t.ok(unmarshalled == jsval, msg); 41 | } 42 | }; 43 | 44 | module.exports = { 45 | do: function(dptid, tests) { 46 | var dpt = DPTLib.resolve(dptid); 47 | var desc = (dpt.hasOwnProperty('subtype') && dpt.subtype.desc) || dpt.basetype.desc; 48 | test(`${dptid}: ${desc}`, function(t) { 49 | Object.keys(tests).forEach(function(key) { 50 | var apdu = new Buffer(tests[key].apdu_data); 51 | var jsval = tests[key].jsval; 52 | //console.log(dptid + ': apdu=%j jsval=%j', apdu, jsval); 53 | // 1. marshalling test (JS value to APDU) 54 | var marshalled_data = marshalTest(t, dptid, jsval, apdu); 55 | // 2. unmarshal from APDU produced by step 1 56 | unmarshalTest(t, dptid, jsval, marshalled_data); 57 | // 3. unmarshal from test APDU 58 | unmarshalTest(t, dptid, jsval, apdu); 59 | }); 60 | t.end(); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/dptlib/dpt7.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT7: 16-bit unsigned integer value 8 | // 9 | exports.basetype = { 10 | "bitlength" : 16, 11 | "signedness": "unsigned", 12 | "valuetype" : "basic", 13 | "desc" : "16-bit unsigned value" 14 | } 15 | 16 | // DPT subtypes info 17 | exports.subtypes = { 18 | // 7.001 pulses 19 | "001" : { "use" : "G", 20 | "name" : "DPT_Value_2_Ucount", 21 | "desc" : "pulses", 22 | "unit" : "pulses" 23 | }, 24 | 25 | // 7.002 time(ms) 26 | "002" : { "use" : "G", 27 | "name" : "DPT_TimePeriodMsec", 28 | "desc" : "time (ms)", 29 | "unit" : "milliseconds" 30 | }, 31 | 32 | // 7.003 time (10ms) 33 | "003" : { "use" : "G", 34 | "name" : "DPT_TimePeriod10Msec", 35 | "desc" : "time (10ms)", 36 | "unit" : "centiseconds" 37 | }, 38 | 39 | // 7.004 time (100ms) 40 | "004" : { "use" : "G", 41 | "name" : "DPT_TimePeriod100Msec", 42 | "desc" : "time (100ms)", 43 | "unit" : "deciseconds" 44 | }, 45 | 46 | // 7.005 time (sec) 47 | "005" : { "use" : "G", 48 | "name" : "DPT_TimePeriodSec", 49 | "desc" : "time (s)", 50 | "unit" : "seconds" 51 | }, 52 | 53 | // 7.006 time (min) 54 | "006" : { "use" : "G", 55 | "name" : "DPT_TimePeriodMin", 56 | "desc" : "time (min)", 57 | "unit" : "minutes" 58 | }, 59 | 60 | // 7.007 time (hour) 61 | "007" : { "use" : "G", 62 | "name" : "DPT_TimePeriodHrs", 63 | "desc" : "time (hrs)", 64 | "unit" : "hours" 65 | }, 66 | 67 | // 7.010 DPT_PropDataType 68 | // not to be used in runtime communications! 69 | "010" : { "use" : "FB", 70 | "name" : "DPT_PropDataType", 71 | "desc" : "Identifier Interface Object Property data type " 72 | }, 73 | 74 | // 7.011 75 | "011" : { "use" : "FB SAB", 76 | "name" : "DPT_Length_mm", 77 | "desc" : "Length in mm", 78 | "unit" : "mm" 79 | }, 80 | 81 | // 7.012 82 | "012" : { "use" : "FB", 83 | "name" : "DPT_UEICurrentmA", 84 | "desc" : "bus power supply current (mA)", 85 | "unit" : "mA" 86 | }, 87 | 88 | // 7.013 89 | "013" : { "use" : "FB", 90 | "name" : "DPT_Brightness", 91 | "desc" : "interior brightness", 92 | "unit" : "lux" 93 | }, 94 | 95 | // 7.600 96 | "600" : { "use" : "FB", 97 | "name" : "DPT_Absolute_Colour_Temperature", 98 | "desc" : "Absolute colour temperature", 99 | "unit" : "K" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/dptlib/dpt18.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | // 7 | // DPT18: 8-bit Scene Control 8 | // 9 | 10 | /* 11 | class DPT18_Frame < DPTFrame 12 | bit1 :exec_learn, { 13 | :display_name : "Execute=0, Learn = 1" 14 | } 15 | bit1 :pad, { 16 | :display_name : "Reserved bit" 17 | } 18 | bit6 :data, { 19 | :display_name : "Scene number" 20 | } 21 | end 22 | */ 23 | 24 | // TODO: implement fromBuffer, formatAPDU 25 | const log = require('log-driver').logger; 26 | 27 | exports.formatAPDU = function (value) { 28 | if (value == null) log.warn("DPT18: cannot write null value"); 29 | else { 30 | var apdu_data = new Buffer(1); 31 | if (typeof value == 'object' && 32 | value.hasOwnProperty("save_recall") && 33 | value.hasOwnProperty("scenenumber")) { 34 | var sSceneNumberbinary = ((value.scenenumber - 1) >>> 0).toString(2); 35 | var sVal = value.save_recall + "0" + sSceneNumberbinary.padStart(6, "0"); 36 | //console.log("BANANA SEND HEX " + sVal.toString("hex").toUpperCase()) 37 | apdu_data[0] = parseInt(sVal, 2);// 0b10111111; 38 | } else { 39 | log.error("DPT18: Must supply a value object of {save_recall, scenenumber}"); 40 | } 41 | return apdu_data; 42 | } 43 | } 44 | 45 | exports.fromBuffer = function (buf) { 46 | //console.log("BANANA BUFF RECEIVE HEX " + buf.toString("hex").toUpperCase()) 47 | if (buf.length != 1) { 48 | log.error("DP18: Buffer should be 1 byte long"); 49 | } else { 50 | var sBit = (parseInt(buf.toString("hex").toUpperCase(), 16).toString(2)).padStart(8, '0'); // Get bit from hex 51 | //console.log("BANANA BUFF RECEIVE BIT " + sBit) 52 | return { 53 | save_recall: sBit.substring(0, 1), 54 | scenenumber: parseInt(sBit.substring(2), 2) + 1 55 | } 56 | }; 57 | } 58 | 59 | // DPT18 basetype info 60 | exports.basetype = { 61 | bitlength: 8, 62 | valuetype: 'composite', 63 | desc: "8-bit Scene Activate/Learn + number" 64 | } 65 | 66 | // DPT9 subtypes 67 | exports.subtypes = { 68 | // 9.001 temperature (oC) 69 | "001": { 70 | name: "DPT_SceneControl", desc: "scene control" 71 | } 72 | } 73 | 74 | 75 | 76 | /* 77 | 02/April/2020 Supergiovane 78 | USE: 79 | Input must be an object: {save_recall, scenenumber} 80 | save_recall: 0 = recall scene, 1 = save scene 81 | scenenumber: the scene number, example 1 82 | Example: {save_recall=0, scenenumber=2} 83 | */ 84 | 85 | -------------------------------------------------------------------------------- /src/dptlib/dpt10.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT10.*: time (3 bytes) 10 | // 11 | const util = require('util'); 12 | const dowTimeRegexp = /((\d)\/)?(\d{1,2}):(\d{1,2}):(\d{1,2})/; 13 | 14 | // DPTFrame to parse a DPT10 frame. 15 | // Always 8-bit aligned. 16 | 17 | exports.formatAPDU = (value) => { 18 | let dow, hour, minute, second; 19 | // day of week. NOTE: JS Sunday = 0 20 | switch (typeof value) { 21 | case 'string': 22 | // try to parse 23 | match = dowTimeRegexp.exec(value); 24 | if (match) { 25 | const currentDoW = ((new Date().getDay() - 7) % 7) + 7; 26 | dow = match[2] != undefined ? parseInt(match[2]) : currentDoW; 27 | hour = parseInt(match[3]); 28 | minute = parseInt(match[4]); 29 | second = parseInt(match[5]); 30 | } else { 31 | log.warn('DPT10: invalid time format (%s)', value); 32 | } 33 | break; 34 | case 'object': 35 | if (value.constructor.name != 'Date') { 36 | log.warn('Must supply a Date or String for DPT10 time'); 37 | break; 38 | } 39 | case 'number': 40 | value = new Date(value); 41 | default: 42 | dow = ((value.getDay() - 7) % 7) + 7; 43 | hour = value.getHours(); 44 | minute = value.getMinutes(); 45 | second = value.getSeconds(); 46 | } 47 | 48 | return Buffer.from([(dow << 5) + hour, minute, second]); 49 | }; 50 | 51 | // return a JS Date from a DPT10 payload, with DOW/hour/month/seconds set to the buffer values. 52 | // The week/month/year are inherited from the current timestamp. 53 | exports.fromBuffer = (buf) => { 54 | if (buf.length != 3) return log.warn('DPT10: Buffer should be 3 bytes long'); 55 | const [dnh, minutes, seconds] = buf; 56 | const dow = (dnh & 0b11100000) >> 5; 57 | const hours = dnh & 0b00011111; 58 | if ( 59 | hours < 0 || 60 | hours > 23 || 61 | minutes < 0 || 62 | minutes > 59 || 63 | seconds < 0 || 64 | seconds > 59 65 | ) 66 | return log.warn( 67 | 'DPT10: buffer %j (decoded as %d:%d:%d) is not a valid time', 68 | buf, 69 | hours, 70 | minutes, 71 | seconds 72 | ); 73 | 74 | const d = new Date(); 75 | if (d.getDay() !== dow) 76 | // adjust day of month to get the day of week right 77 | d.setDate(d.getDate() + dow - d.getDay()); 78 | // TODO: Shouldn't this be UTCHours? 79 | d.setHours(hours, minutes, seconds); 80 | return d; 81 | }; 82 | 83 | // DPT10 base type info 84 | exports.basetype = { 85 | bitlength: 24, 86 | valuetype: 'composite', 87 | desc: 'day of week + time of day', 88 | }; 89 | 90 | // DPT10 subtypes info 91 | exports.subtypes = { 92 | // 10.001 time of day 93 | '001': { 94 | name: 'DPT_TimeOfDay', 95 | desc: 'time of day', 96 | }, 97 | }; 98 | -------------------------------------------------------------------------------- /test/wiredtests/test-readstorm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const knx = require('../..'); 7 | const test = require('tape'); 8 | const util = require('util'); 9 | const options = require('./wiredtest-options.js'); 10 | 11 | Error.stackTraceLimit = Infinity; 12 | 13 | /* 14 | ========== ================== 15 | this is a WIRED test and requires a real KNX IP router on the LAN 16 | ========== ================== 17 | to run all tests: $ WIREDTEST=1 npm test 18 | to run one test : $ WIREDTEST=1 node test/wiredtests/.js 19 | */ 20 | if (process.env.hasOwnProperty('WIREDTEST')) { 21 | test('KNX wired test - read multiple statuses from a consecutive GA range', function(t) { 22 | var readback = {}; 23 | function setupDatapoint(groupadress, statusga) { 24 | var dp = new knx.Datapoint({ 25 | ga: groupadress, 26 | status_ga: statusga, 27 | dpt: "DPT1.001", 28 | autoread: true 29 | }, connection); 30 | dp.on('change', (oldvalue, newvalue) => { 31 | console.log("**** %s current value: %j", groupadress, newvalue); 32 | }); 33 | return dp; 34 | } 35 | function setupDatapoints() { 36 | var ctrl_ga_arr = options.readstorm_control_ga_start.split('/'); 37 | var stat_ga_arr = options.readstorm_status_ga_start.split('/'); 38 | for (i=0; i< options.readstorm_range; i++) { 39 | var ctrl_ga = [ctrl_ga_arr[0], ctrl_ga_arr[1], i+parseInt(ctrl_ga_arr[2])].join('/'); 40 | var stat_ga = [stat_ga_arr[0], stat_ga_arr[1], i+parseInt(stat_ga_arr[2])].join('/'); 41 | setupDatapoint(ctrl_ga, stat_ga); 42 | } 43 | } 44 | var connection = knx.Connection({ 45 | loglevel: 'warn', 46 | //forceTunneling: true, 47 | // minimumDelay: 100, 48 | handlers: { 49 | connected: function() { 50 | setupDatapoints(); 51 | }, 52 | event: function (evt, src, dest, value) { 53 | if (evt == 'GroupValue_Response') { 54 | readback[dest] = [src, value]; 55 | // have we got responses from all the read requests for all datapoints? 56 | if (Object.keys(readback).length == options.readstorm_range) { 57 | t.pass(util.format('readstorm: all %d datapoints accounted for', options.readstorm_range)); 58 | t.end(); 59 | process.exit(0); 60 | } 61 | } 62 | }, 63 | error: function(connstatus) { 64 | console.log("%s **** ERROR: %j", 65 | new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), 66 | connstatus); 67 | process.exit(1); 68 | } 69 | } 70 | }); 71 | }); 72 | 73 | setTimeout(function() { 74 | console.log('Exiting with timeout...'); 75 | process.exit(2); 76 | }, 1500); 77 | } 78 | -------------------------------------------------------------------------------- /test/wiredtests/test-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | Error.stackTraceLimit = Infinity; 6 | 7 | const knx = require('../..'); 8 | const address = require('../../src/Address.js'); 9 | const assert = require('assert'); 10 | const test = require('tape'); 11 | 12 | const options = require('./wiredtest-options.js'); 13 | /* 14 | ========== ================== 15 | this is a WIRED test and requires a real KNX IP router on the LAN 16 | ========== ================== 17 | 18 | $ WIREDTEST=1 node test/wiredtests/.js 19 | */ 20 | if (process.env.hasOwnProperty('WIREDTEST')) { 21 | 22 | test('KNX wired test - control a basic DPT1 binary switch', function(t) { 23 | var counter = 0; 24 | var connection = new knx.Connection({ 25 | debug: true, 26 | physAddr: options.physAddr, 27 | handlers: { 28 | connected: function() { 29 | console.log('----------'); 30 | console.log('Connected!'); 31 | console.log('----------'); 32 | var light = new knx.Datapoint({ 33 | ga: options.wired_test_control_ga, 34 | dpt: 'DPT1.001' 35 | }, connection); 36 | light.on('change', () => { 37 | counter += 1; 38 | if (counter == 4) { 39 | t.pass('all 4 responses received'); 40 | t.end(); 41 | process.exit(0); 42 | } 43 | }); 44 | // operation 1 45 | light.write(0); 46 | // operation 2 47 | setTimeout(function() { 48 | light.write(1); 49 | }, 500); 50 | // issue #71 - writing to an invalid address should not stall the FSM 51 | connection.write('10/10/5', 1); 52 | // operation 3 - Do the same with writeRaw 53 | setTimeout(function() { 54 | connection.writeRaw(options.wired_test_control_ga, new Buffer('00', 'hex'), 1); 55 | }, 1000); 56 | // operation 4 - Do the same with writeRaw 57 | setTimeout(function() { 58 | connection.writeRaw(options.wired_test_control_ga, new Buffer('01', 'hex'), 0); 59 | }, 1500); 60 | }, 61 | event: function(evt, src, dest, value) { 62 | console.log("%s ===> %s <===, src: %j, dest: %j, value: %j", 63 | new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), 64 | evt, src, dest, value 65 | ); 66 | }, 67 | error: function(connstatus) { 68 | console.log("%s **** ERROR: %j", 69 | new Date().toISOString().replace(/T/, ' ').replace(/Z$/, ''), 70 | connstatus); 71 | t.fail('error: '+connstatus); 72 | process.exit(1); 73 | } 74 | } 75 | }); 76 | }) 77 | 78 | setTimeout(function() { 79 | console.log('Exiting with timeout ...'); 80 | process.exit(2); 81 | }, 2000); 82 | } 83 | -------------------------------------------------------------------------------- /README-datapoints.md: -------------------------------------------------------------------------------- 1 | ## Datapoint Types 2 | 3 | |DPT | Description | Value type | Example | Notes | 4 | |--- |--- |--- |--- |--- | 5 | |DPT1 | 1-bit control | Boolean/Numeric | true/"true"/1 false/"false"/0 | | 6 | |DPT2 | 1-bit control w/prio | Object | {priority: 0, data: 1} | | 7 | |DPT3 | 4-bit dimming/blinds | Object | {decr_incr: 1, data: 0} | data: 3-bit (0..7)| 8 | |DPT4 | 8-bit character | String | "a" | 1st char must be ASCII | 9 | |DPT5 | 8-bit unsigned int | Numeric | 127 | 0..255 | 10 | |DPT6 | 8-bit signed int | Numeric | -12 | -128..127 | 11 | |DPT7 | 16-bit unsigned int | Numeric | | 0..65535 | 12 | |DPT8 | 16-bit signed integer | Numeric | | -32768..32767 | 13 | |DPT9 | 16-bit floating point | Numeric | | | 14 | |DPT10 | 24-bit time + day of week | Date | new Date() | only the time part is used, see note | 15 | |DPT11 | 24-bit date | Date | new Date() | only the date part is used, see note | 16 | |DPT12 | 32-bit unsigned int | Numeric | | | 17 | |DPT13 | 32-bit signed int | Numeric | | | 18 | |DPT14 | 32-bit floating point | Numeric | | incomplete: subtypes | 19 | |DPT15 | 32-bit access control | | | incomplete| 20 | |DPT16 | ASCII string | String | | | 21 | |DPT17 | Scene number | | | incomplete| 22 | |DPT18 | Scene control | | | incomplete| 23 | |DPT19 | 8-byte Date and Time | Date | new Date() | | 24 | |DPT20-255 | feel free to contribute! | | | | 25 | 26 | 27 | When you add new DPT's, please ensure that you add the corresponding unit test 28 | under the `test/dptlib` subdirectory. The unit tests come with a small helper 29 | library that provides the boilerplate code to marshal and unlarshal your test cases. 30 | 31 | Take for example the unit test for DPT5, which carries a single-byte payload. 32 | Some of its subtypes (eg. 5.001 for percentages and 5.003 for angle degrees) 33 | need to be scaled up or down, whereas other subtypes *must not* be scaled at all: 34 | 35 | ```js 36 | // DPT5 without subtype: no scaling 37 | commontest.do('DPT5', [ 38 | { apdu_data: [0x00], jsval: 0}, 39 | { apdu_data: [0x40], jsval: 64}, 40 | { apdu_data: [0x41], jsval: 65}, 41 | { apdu_data: [0x80], jsval: 128}, 42 | { apdu_data: [0xff], jsval: 255} 43 | ]); 44 | // 5.001 percentage (0=0..ff=100%) 45 | commontest.do('DPT5.001', [ 46 | { apdu_data: [0x00], jsval: 0 }, 47 | { apdu_data: [0x80], jsval: 50}, 48 | { apdu_data: [0xff], jsval: 100} 49 | ]); 50 | // 5.003 angle (degrees 0=0, ff=360) 51 | commontest.do('DPT5.003', [ 52 | { apdu_data: [0x00], jsval: 0 }, 53 | { apdu_data: [0x80], jsval: 181 }, 54 | { apdu_data: [0xff], jsval: 360 } 55 | ]); 56 | ``` 57 | 58 | ## Date and time DPTs (DPT10, DPT11) 59 | Please have in mind that Javascript and KNX have very different base type for time and date. 60 | 61 | - DPT10 is time (hh:mm:ss) plus "day of week". This concept is unavailable in JS, so you'll be getting/setting a regular *Date* Js object, but *please remember* you'll need to _ignore_ the date, month and year. The *exact same datagram* that converts to "Mon, Jul 1st 12:34:56", will evaluate to a wildly different JS Date of "Mon, Jul 8th 12:34:56" one week later. Be warned! 62 | - DPT11 is date (dd/mm/yyyy): the same applies for DPT11, you'll need to *ignore the time part*. -------------------------------------------------------------------------------- /README-resilience.md: -------------------------------------------------------------------------------- 1 | ## And why should I bother? 2 | 3 | The main cause for writing my own KNX protocol stack is that I couldn't find a *robust* access layer that properly handles state management. 4 | Connections tend to fail all the time; consider flakey Wi-Fi, RRR's (Recalcitrant Rebooting Routers), bad karma, it happens all the time. A KNX access layer should be *resilient* and be able to recover if needed. 5 | 6 | Also, although seemingly innocent, the consecutive calls to *read()* and then *write()* on the same group address will either *confuse* your KNX IP router, or *return incoherent results*. 7 | KNXnet/IP uses **UDP** sockets, which is not ideal from a programmer's perspective. Packets can come and go in any order; very few libraries offer the robustness to reconcile state and ensure a **steady and reliable connection**. 8 | 9 | This library is, to the best of my knowledge, the only one that can handle the *serialisation* of tunneling requests in a way that your program will have a *robust and reliable* KNX connection. Try toggling your Wi-Fi or disconnect your Ethernet cable while you're connected; the library will detect this and reconnect when network access is restored :) 10 | 11 | ``` 12 | 27 Oct 15:44:24 - [info] Started flows 13 | 27 Oct 15:44:24 - [info] [knx-controller:9ab91ab8.547938] KNX: successfully connected to 224.0.23.12:3671 14 | 27 Oct 15:44:24 - [info] [knx-controller:9ab91ab8.547938] GroupValue_Read {"srcphy":"15.15.15","dstgad":"0/0/15"} 15 | ... 16 | 27 Oct 15:44:54 - [info] [knx-controller:9ab91ab8.547938] KNX Connection Error: timed out waiting for CONNECTIONSTATE_RESPONSE 17 | 27 Oct 15:45:36 - [info] [knx-controller:9ab91ab8.547938] KNX: successfully connected to 224.0.23.12:3671 18 | 27 Oct 15:45:36 - [info] [knx-in:input] GroupValue_Read {"srcphy":"15.15.15","dstgad":"0/0/15"} 19 | ``` 20 | 21 | 22 | ## A note on resilience 23 | 24 | There are basically *two* ways to talk to KNX via UDP/IP: 25 | 26 | - **Tunneling** is effectively UDP **unicast** with connection state (essentially mimicking TCP), thus we get to make a CONNECT_REQUEST to establish a session id with the router or interface. This enables us to periodically check on the connection's health (with CONNECTIONSTATE_REQUESTs) and handle retries, acknowledgements etc. The disadvantage here is that KNXnet/IP lacks a service discovery mechanism, thus you need to specify the router/interface endpoint IP address and port. 27 | 28 | - **Routing** is a plain UDP multicast transport *without any connection or reliability semantics whatsoever* - which makes it much harder to detect dropped packets eg due to congested networks. The multicast approach works well on wired high-speed (eg Ethernet) segments that are dedicated to KNX traffic only. As we all know, KNX/TP1 has a bandwidth that is several orders of magnitude slower than a LAN, but this isn't necessarily the case when you connect over a VPN! *In reality, your network is definately going to drop some packets*. The advantage of multicast though is that it needs no configuration, as long as the IP router is configured to the default KNX multicast address (224.0.23.12) 29 | 30 | - 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: 31 | 32 | ```js 33 | var connection = new knx.Connection( { 34 | // use tunneling with multicast - this is NOT supported by all routers! 35 | forceTunneling: true, 36 | ... 37 | }); 38 | ``` 39 | -------------------------------------------------------------------------------- /src/dptlib/dpt2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // DPT2 frame description. 9 | // Always 8-bit aligned. 10 | exports.formatAPDU = (value) => { 11 | if (value == null) return log.error('DPT2: cannot write null value'); 12 | 13 | if ( 14 | typeof value === 'object' && 15 | value.hasOwnProperty('priority') && 16 | value.hasOwnProperty('data') 17 | ) 18 | return Buffer.from([(value.priority << 1) + (value.data & 0b00000001)]); 19 | 20 | log.error('DPT2: Must supply an value {priority:, data:}'); 21 | // FIXME: should this return zero buffer when error? Or nothing? 22 | return Buffer.from([0]); 23 | }; 24 | 25 | exports.fromBuffer = (buf) => { 26 | if (buf.length !== 1) return log.error('Buffer should be 1 byte long'); 27 | 28 | return { 29 | priority: (buf[0] & 0b00000010) >> 1, 30 | data: buf[0] & 0b00000001, 31 | }; 32 | }; 33 | 34 | // DPT basetype info hash 35 | exports.basetype = { 36 | bitlength: 2, 37 | valuetype: 'composite', 38 | desc: '1-bit value with priority', 39 | }; 40 | 41 | // DPT subtypes info hash 42 | exports.subtypes = { 43 | // 2.001 switch control 44 | '001': { 45 | use: 'G', 46 | name: 'DPT_Switch_Control', 47 | desc: 'switch with priority', 48 | enc: { 0: 'Off', 1: 'On' }, 49 | }, 50 | // 2.002 boolean control 51 | '002': { 52 | use: 'G', 53 | name: 'DPT_Bool_Control', 54 | desc: 'boolean with priority', 55 | enc: { 0: 'false', 1: 'true' }, 56 | }, 57 | // 2.003 enable control 58 | '003': { 59 | use: 'FB', 60 | name: 'DPT_Emable_Control', 61 | desc: 'enable with priority', 62 | enc: { 0: 'Disabled', 1: 'Enabled' }, 63 | }, 64 | 65 | // 2.004 ramp control 66 | '004': { 67 | use: 'FB', 68 | name: 'DPT_Ramp_Control', 69 | desc: 'ramp with priority', 70 | enc: { 0: 'No ramp', 1: 'Ramp' }, 71 | }, 72 | 73 | // 2.005 alarm control 74 | '005': { 75 | use: 'FB', 76 | name: 'DPT_Alarm_Control', 77 | desc: 'alarm with priority', 78 | enc: { 0: 'No alarm', 1: 'Alarm' }, 79 | }, 80 | 81 | // 2.006 binary value control 82 | '006': { 83 | use: 'FB', 84 | name: 'DPT_BinaryValue_Control', 85 | desc: 'binary value with priority', 86 | enc: { 0: 'Off', 1: 'On' }, 87 | }, 88 | 89 | // 2.007 step control 90 | '007': { 91 | use: 'FB', 92 | name: 'DPT_Step_Control', 93 | desc: 'step with priority', 94 | enc: { 0: 'Off', 1: 'On' }, 95 | }, 96 | 97 | // 2.008 Direction1 control 98 | '008': { 99 | use: 'FB', 100 | name: 'DPT_Direction1_Control', 101 | desc: 'direction 1 with priority', 102 | enc: { 0: 'Off', 1: 'On' }, 103 | }, 104 | 105 | // 2.009 Direction2 control 106 | '009': { 107 | use: 'FB', 108 | name: 'DPT_Direction2_Control', 109 | desc: 'direction 2 with priority', 110 | enc: { 0: 'Off', 1: 'On' }, 111 | }, 112 | 113 | // 2.010 start control 114 | '001': { 115 | use: 'FB', 116 | name: 'DPT_Start_Control', 117 | desc: 'start with priority', 118 | enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, 119 | }, 120 | 121 | // 2.011 state control 122 | '001': { 123 | use: 'FB', 124 | name: 'DPT_Switch_Control', 125 | desc: 'switch', 126 | enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, 127 | }, 128 | 129 | // 2.012 invert control 130 | '001': { 131 | use: 'FB', 132 | name: 'DPT_Switch_Control', 133 | desc: 'switch', 134 | enc: { 0: 'No control', 1: 'No control', 2: 'Off', 3: 'On' }, 135 | }, 136 | }; 137 | -------------------------------------------------------------------------------- /src/dptlib/dpt14.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT14.*: 4-byte floating point value 10 | // 11 | 12 | /* In sharp contrast to DPT9 (16-bit floating point - JS spec does not support), 13 | * the case for 32-bit floating point is simple... 14 | */ 15 | 16 | exports.formatAPDU = (value) => { 17 | if (value == null || typeof value != 'number') 18 | log.error('DPT14: Must supply a number value'); 19 | const apdu_data = Buffer.alloc(4); 20 | apdu_data.writeFloatBE(value, 0); 21 | return apdu_data; 22 | }; 23 | 24 | exports.fromBuffer = (buf) => { 25 | if (buf.length != 4) log.warn('DPT14: Buffer should be 4 bytes long'); 26 | return buf.readFloatBE(0); 27 | }; 28 | 29 | // DPT14 base type info 30 | exports.basetype = { 31 | bitlength: 32, 32 | valuetype: 'basic', 33 | range: [0, Math.pow(2, 32)], 34 | desc: '32-bit floating point value', 35 | }; 36 | 37 | // DPT14 subtypes info 38 | exports.subtypes = { 39 | // TODO 40 | '007': { 41 | name: 'DPT_Value_AngleDeg°', 42 | desc: 'angle, degree', 43 | unit: '°', 44 | }, 45 | 46 | '019': { 47 | name: 'DPT_Value_Electric_Current', 48 | desc: 'electric current', 49 | unit: 'A', 50 | }, 51 | 52 | '027': { 53 | name: 'DPT_Value_Electric_Potential', 54 | desc: 'electric potential', 55 | unit: 'V', 56 | }, 57 | 58 | '028': { 59 | name: 'DPT_Value_Electric_PotentialDifference', 60 | desc: 'electric potential difference', 61 | unit: 'V', 62 | }, 63 | 64 | '031': { 65 | name: 'DPT_Value_Energ', 66 | desc: 'energy', 67 | unit: 'J', 68 | }, 69 | 70 | '032': { 71 | name: 'DPT_Value_Force', 72 | desc: 'force', 73 | unit: 'N', 74 | }, 75 | 76 | '033': { 77 | name: 'DPT_Value_Frequency', 78 | desc: 'frequency', 79 | unit: 'Hz', 80 | }, 81 | 82 | '036': { 83 | name: 'DPT_Value_Heat_FlowRate', 84 | desc: 'heat flow rate', 85 | unit: 'W', 86 | }, 87 | 88 | '037': { 89 | name: 'DPT_Value_Heat_Quantity', 90 | desc: 'heat, quantity of', 91 | unit: 'J', 92 | }, 93 | 94 | '038': { 95 | name: 'DPT_Value_Impedance', 96 | desc: 'impedance', 97 | unit: 'Ω', 98 | }, 99 | 100 | '039': { 101 | name: 'DPT_Value_Length', 102 | desc: 'length', 103 | unit: 'm', 104 | }, 105 | 106 | '051': { 107 | name: 'DPT_Value_Mass', 108 | desc: 'mass', 109 | unit: 'kg', 110 | }, 111 | 112 | '056': { 113 | name: 'DPT_Value_Power', 114 | desc: 'power', 115 | unit: 'W', 116 | }, 117 | 118 | '065': { 119 | name: 'DPT_Value_Speed', 120 | desc: 'speed', 121 | unit: 'm/s', 122 | }, 123 | 124 | '066': { 125 | name: 'DPT_Value_Stress', 126 | desc: 'stress', 127 | unit: 'Pa', 128 | }, 129 | 130 | '067': { 131 | name: 'DPT_Value_Surface_Tension', 132 | desc: 'surface tension', 133 | unit: '1/Nm', 134 | }, 135 | 136 | '068': { 137 | name: 'DPT_Value_Common_Temperature', 138 | desc: 'temperature, common', 139 | unit: '°C', 140 | }, 141 | 142 | '069': { 143 | name: 'DPT_Value_Absolute_Temperature', 144 | desc: 'temperature (absolute)', 145 | unit: 'K', 146 | }, 147 | 148 | '070': { 149 | name: 'DPT_Value_TemperatureDifference', 150 | desc: 'temperature difference', 151 | unit: 'K', 152 | }, 153 | 154 | '078': { 155 | name: 'DPT_Value_Weight', 156 | desc: 'weight', 157 | unit: 'N', 158 | }, 159 | 160 | '079': { 161 | name: 'DPT_Value_Work', 162 | desc: 'work', 163 | unit: 'J', 164 | }, 165 | }; 166 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as events from "events"; 4 | 5 | type HandlersSpec = { 6 | connected?: () => void; 7 | disconnected?: () => void; 8 | event?: ( 9 | evt: string, 10 | src: KnxDeviceAddress, 11 | dest: KnxGroupAddress, 12 | value: Buffer 13 | ) => void; 14 | error?: (connstatus: any) => void; 15 | }; 16 | 17 | type ConnectionSpec = { 18 | /** ip address of the KNX router or interface */ 19 | ipAddr?: string; 20 | /** port of the KNX router or interface */ 21 | ipPort?: number; 22 | /** in case you need to specify the multicast interface (say if you have more than one) */ 23 | interface?: string; 24 | /** the KNX physical address we'd like to use */ 25 | physAddr?: string; 26 | /** set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. */ 27 | loglevel?: string; 28 | /** do not automatically connect, but use connection.Connect() to establish connection */ 29 | manualConnect?: boolean; 30 | /** use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md */ 31 | forceTunneling?: boolean; 32 | /** wait at least 10 millisec between each datagram */ 33 | minimumDelay?: number; 34 | /** enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this */ 35 | suppress_ack_ldatareq?: boolean; 36 | /** 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. */ 37 | localEchoInTunneling?: boolean; 38 | /** event handlers. You can also bind them later with connection.on(event, fn) */ 39 | handlers?: HandlersSpec; 40 | }; 41 | 42 | type KnxDeviceAddress = string; 43 | 44 | type KnxGroupAddress = string; 45 | 46 | /** The type of the KnxValue depends on the DPT that it is associated with */ 47 | type KnxValue = number | string | boolean | Date; 48 | 49 | /** Possible formats "X" or "X.Y", i.e. "1" or "1.001" */ 50 | type DPT = string; 51 | 52 | type DatapointOptions = { 53 | ga: KnxGroupAddress; 54 | dpt?: DPT; 55 | autoread?: boolean; 56 | }; 57 | 58 | interface DatapointEvent { 59 | on( 60 | event: "change", 61 | listener: (old_value: KnxValue, new_value: KnxValue) => void 62 | ): this; 63 | on(event: string, listener: (event: string, value: any) => void): this; 64 | } 65 | 66 | declare module "knx" { 67 | type MachinaEventsCallback = (...args: any[]) => void; 68 | 69 | interface MachinaEventsReturn { 70 | eventName: string; 71 | callback: MachinaEventsCallback; 72 | off: () => void; 73 | } 74 | 75 | class MachinaEvents { 76 | emit(eventName: string): void; 77 | on(eventName: string, callback: MachinaEventsCallback): MachinaEventsReturn; 78 | off(eventName?: string, callback?: MachinaEventsCallback): void; 79 | } 80 | 81 | interface MachinaEventsReturn { 82 | eventName: string; 83 | callback: MachinaEventsCallback; 84 | off: () => void; 85 | } 86 | 87 | export interface IConnection extends MachinaEvents { 88 | debug: boolean; 89 | Connect(): void; 90 | Disconnect(cb?: () => void): void; 91 | read(ga: KnxGroupAddress, cb?: (src: KnxDeviceAddress, value: Buffer) => void): void; 92 | write(ga: KnxGroupAddress, value: Buffer, dpt: DPT, cb?: () => void): void; 93 | } 94 | 95 | export class Connection extends MachinaEvents implements IConnection { 96 | public debug: boolean; 97 | constructor(conf: ConnectionSpec); 98 | Connect(): void; 99 | Disconnect(cb?: () => void): void; 100 | read(ga: KnxGroupAddress, cb?: (src: KnxDeviceAddress, value: Buffer) => void): void; 101 | write(ga: KnxGroupAddress, value: Buffer, dpt: DPT, cb?: () => void): void; 102 | writeRaw( 103 | ga: KnxGroupAddress, 104 | value: Buffer, 105 | bitlength?: number, 106 | cb?: () => void 107 | ): void; 108 | } 109 | 110 | export class Datapoint extends events.EventEmitter implements DatapointEvent { 111 | readonly current_value: KnxValue; 112 | readonly dptid: DPT; 113 | 114 | constructor(options: DatapointOptions, conn?: IConnection); 115 | bind(conn: Connection): void; 116 | write(value: KnxValue): void; 117 | read(callback?: (src: KnxDeviceAddress, value: KnxValue) => void): void; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/KnxConstants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const KnxLog = require('./KnxLog'); 7 | 8 | // SOURCES: 9 | // http://www.eb-systeme.de/?page_id=479 10 | // http://knxnetipdissect.sourceforge.net/doc.html 11 | // http://dz.prosyst.com/pdoc/mBS_SH_SDK_7.3.0/modules/knx/api/com/prosyst/mbs/services/knx/ip/Constants.html 12 | 13 | const SERVICE_TYPE = { 14 | SEARCH_REQUEST: 0x0201, 15 | SEARCH_RESPONSE: 0x0202, 16 | DESCRIPTION_REQUEST: 0x0203, 17 | DESCRIPTION_RESPONSE: 0x0204, 18 | CONNECT_REQUEST: 0x0205, 19 | CONNECT_RESPONSE: 0x0206, 20 | CONNECTIONSTATE_REQUEST: 0x0207, 21 | CONNECTIONSTATE_RESPONSE: 0x0208, 22 | DISCONNECT_REQUEST: 0x0209, 23 | DISCONNECT_RESPONSE: 0x020a, 24 | DEVICE_CONFIGURATION_REQUEST: 0x0310, 25 | DEVICE_CONFIGURATION_ACK: 0x0311, 26 | TUNNELING_REQUEST: 0x0420, 27 | TUNNELING_ACK: 0x0421, 28 | ROUTING_INDICATION: 0x0530, 29 | ROUTING_LOST_MESSAGE: 0x0531, 30 | UNKNOWN: -1, 31 | }; 32 | // 33 | const CONNECTION_TYPE = { 34 | DEVICE_MGMT_CONNECTION: 0x03, 35 | TUNNEL_CONNECTION: 0x04, 36 | REMOTE_LOGGING_CONNECTION: 0x06, 37 | REMOTE_CONFIGURATION_CONNECTION: 0x07, 38 | OBJECT_SERVER_CONNECTION: 0x08, 39 | }; 40 | // 41 | const PROTOCOL_TYPE = { 42 | IPV4_UDP: 0x01, 43 | IPV4_TCP: 0x02, 44 | }; 45 | // 46 | const KNX_LAYER = { 47 | LINK_LAYER: 0x02 /** Tunneling on link layer, establishes a link layer tunnel to the KNX network.*/, 48 | RAW_LAYER: 0x04 /** Tunneling on raw layer, establishes a raw tunnel to the KNX network. */, 49 | BUSMONITOR_LAYER: 0x80 /** Tunneling on busmonitor layer, establishes a busmonitor tunnel to the KNX network.*/, 50 | }; 51 | 52 | const FRAMETYPE = { 53 | EXTENDED: 0x00, 54 | STANDARD: 0x01, 55 | }; 56 | 57 | //https://github.com/calimero-project/calimero-core/blob/master/src/tuwien/auto/calimero/knxnetip/servicetype/ErrorCodes.java 58 | const RESPONSECODE = { 59 | NO_ERROR: 0x00, // E_NO_ERROR - The connection was established succesfully 60 | E_HOST_PROTOCOL_TYPE: 0x01, 61 | E_VERSION_NOT_SUPPORTED: 0x02, 62 | E_SEQUENCE_NUMBER: 0x04, 63 | E_CONNSTATE_LOST: 0x15, // typo in eibd/libserver/eibnetserver.cpp:394, forgot 0x prefix ??? "uchar res = 21;" 64 | E_CONNECTION_ID: 0x21, // - The KNXnet/IP server device could not find an active data connection with the given ID 65 | E_CONNECTION_TYPE: 0x22, // - The requested connection type is not supported by the KNXnet/IP server device 66 | E_CONNECTION_OPTION: 0x23, // - The requested connection options is not supported by the KNXnet/IP server device 67 | E_NO_MORE_CONNECTIONS: 0x24, // - The KNXnet/IP server could not accept the new data connection (Maximum reached) 68 | E_DATA_CONNECTION: 0x26, // - The KNXnet/IP server device detected an erro concerning the Dat connection with the given ID 69 | E_KNX_CONNECTION: 0x27, // - The KNXnet/IP server device detected an error concerning the KNX Bus with the given ID 70 | E_TUNNELING_LAYER: 0x29, 71 | }; 72 | 73 | const MESSAGECODES = { 74 | 'L_Raw.req': 0x10, 75 | 'L_Data.req': 0x11, 76 | 'L_Poll_Data.req': 0x13, 77 | 'L_Poll_Data.con': 0x25, 78 | 'L_Data.ind': 0x29, 79 | 'L_Busmon.ind': 0x2b, 80 | 'L_Raw.ind': 0x2d, 81 | 'L_Data.con': 0x2e, 82 | 'L_Raw.con': 0x2f, 83 | 'ETS.Dummy1': 0xc1, // UNKNOWN: see https://bitbucket.org/ekarak/knx.js/issues/23 84 | }; 85 | 86 | const APCICODES = [ 87 | 'GroupValue_Read', 88 | 'GroupValue_Response', 89 | 'GroupValue_Write', 90 | 'PhysicalAddress_Write', 91 | 'PhysicalAddress_Read', 92 | 'PhysicalAddress_Response', 93 | 'ADC_Read', 94 | 'ADC_Response', 95 | 'Memory_Read', 96 | 'Memory_Response', 97 | 'Memory_Write', 98 | 'UserMemory', 99 | 'DeviceDescriptor_Read', 100 | 'DeviceDescriptor_Response', 101 | 'Restart', 102 | 'OTHER', 103 | ]; 104 | 105 | const KnxConstants = { 106 | SERVICE_TYPE, 107 | CONNECTION_TYPE, 108 | PROTOCOL_TYPE, 109 | KNX_LAYER, 110 | FRAMETYPE, 111 | RESPONSECODE, 112 | MESSAGECODES, 113 | }; 114 | 115 | /* TODO helper function to print enum keys */ 116 | const keyText = (mapref, value) => { 117 | // pass in map by name or value 118 | const map = KnxConstants[mapref] || mapref; 119 | if (typeof map !== 'object') throw 'Unknown map: ' + mapref; 120 | for (const [key, v] of Object.entries(map)) if (v == value) return key; 121 | 122 | KnxLog.get().trace('not found: %j', value); 123 | }; 124 | 125 | module.exports = { 126 | ...KnxConstants, 127 | APCICODES, 128 | keyText, 129 | }; 130 | -------------------------------------------------------------------------------- /src/Datapoint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const util = require('util'); 7 | const DPTLib = require('./dptlib'); 8 | const KnxLog = require('./KnxLog'); 9 | const { EventEmitter } = require('events'); 10 | 11 | /* 12 | * A Datapoint is always bound to: 13 | * - a group address (eg. '1/2/3') 14 | * - (optionally) a datapoint type (defaults to DPT1.001) 15 | * You can also supply a valid connection to skip calling bind() 16 | */ 17 | class Datapoint extends EventEmitter { 18 | constructor(options, conn) { 19 | if (options == null || options.ga == null) 20 | throw 'must supply at least { ga, dpt }!'; 21 | super(); 22 | 23 | this.options = options; 24 | this.dptid = options.dpt || 'DPT1.001'; 25 | this.dpt = DPTLib.resolve(this.dptid); 26 | KnxLog.get().trace('resolved %s to %j', this.dptid, this.dpt); 27 | this.current_value = null; 28 | if (conn) this.bind(conn); 29 | } 30 | 31 | /* 32 | * Bind the datapoint to a bus connection 33 | */ 34 | bind(conn) { 35 | if (!conn) throw 'must supply a valid KNX connection to bind to'; 36 | this.conn = conn; 37 | // bind generic event handler for our group address 38 | const gaevent = util.format('event_%s', this.options.ga); 39 | conn.on(gaevent, (evt, src, buf) => { 40 | // get the Javascript value from the raw buffer, if the DPT defines fromBuffer() 41 | switch (evt) { 42 | case 'GroupValue_Write': 43 | case 'GroupValue_Response': 44 | if (buf) { 45 | const jsvalue = DPTLib.fromBuffer(buf, this.dpt); 46 | this.emit('event', evt, jsvalue, src); 47 | this.update(jsvalue, src); // update internal state 48 | } 49 | break; 50 | default: 51 | this.emit('event', evt, src); 52 | // TODO: add default handler; maybe emit warning? 53 | } 54 | }); 55 | // issue a GroupValue_Read request to try to get the initial state from the bus (if any) 56 | if (this.options.autoread) 57 | if (conn.conntime) { 58 | // immediately or... 59 | this.read(); 60 | } else { 61 | // ... when the connection is established 62 | conn.on('connected', () => { 63 | this.read(); 64 | }); 65 | } 66 | } 67 | 68 | update(jsvalue, src) { 69 | const old_value = this.current_value; 70 | if (old_value === jsvalue) return; 71 | 72 | this.emit('change', this.current_value, jsvalue, this.options.ga, src); 73 | this.current_value = jsvalue; // TODO: This should probably change before the event is emitted 74 | const ts = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); // TODO: Why are we timestamping, the logger formatter already timestamps. 75 | KnxLog.get().trace( 76 | '%s **** %s DATAPOINT CHANGE (was: %j)', 77 | ts, 78 | this.toString(), 79 | old_value 80 | ); 81 | } 82 | 83 | /* format a Javascript value into the APDU format dictated by the DPT 84 | and submit a GroupValue_Write to the connection */ 85 | write(value) { 86 | if (!this.conn) throw 'must supply a valid KNX connection to bind to'; 87 | if (this.dpt.hasOwnProperty('range')) { 88 | // check if value is in range 89 | const { range } = this.dpt.basetype; 90 | const [min, max] = range; 91 | if (value < min || value > max) { 92 | throw util.format( 93 | 'Value %j(%s) out of bounds(%j) for %s', 94 | value, 95 | typeof value, 96 | range, 97 | this.dptid 98 | ); 99 | } 100 | } 101 | this.conn.write( 102 | this.options.ga, 103 | value, 104 | this.dptid, 105 | // once we've written to the bus, update internal state 106 | () => this.update(value) 107 | ); 108 | } 109 | 110 | /* 111 | * Issue a GroupValue_Read request to the bus for this datapoint 112 | * use the optional callback() to get notified upon response 113 | */ 114 | read(callback) { 115 | if (!this.conn) throw 'must supply a valid KNX connection to bind to'; 116 | this.conn.read(this.options.ga, (src, buf) => { 117 | const jsvalue = DPTLib.fromBuffer(buf, this.dpt); 118 | if (typeof callback == 'function') callback(src, jsvalue); 119 | }); 120 | } 121 | 122 | toString() { 123 | return util.format( 124 | '(%s) %s %s', 125 | this.options.ga, 126 | this.current_value, 127 | (this.dpt.subtype && this.dpt.subtype.unit) || '' 128 | ); 129 | } 130 | } 131 | 132 | module.exports = Datapoint; 133 | -------------------------------------------------------------------------------- /src/Address.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | const KnxLog = require('./KnxLog'); 6 | const Parser = require('binary-parser').Parser; 7 | 8 | // +-----------------------------------------------+ 9 | // 16 bits | INDIVIDUAL ADDRESS | 10 | // +-----------------------+-----------------------+ 11 | // | OCTET 0 (high byte) | OCTET 1 (low byte) | 12 | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 13 | // bits | 7| 6| 5| 4| 3| 2| 1| 0| 7| 6| 5| 4| 3| 2| 1| 0| 14 | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 15 | // | Subnetwork Address | | 16 | // +-----------+-----------+ Device Address | 17 | // |(Area Adrs)|(Line Adrs)| | 18 | // +-----------------------+-----------------------+ 19 | 20 | // +-----------------------------------------------+ 21 | // 16 bits | GROUP ADDRESS (3 level) | 22 | // +-----------------------+-----------------------+ 23 | // | OCTET 0 (high byte) | OCTET 1 (low byte) | 24 | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 25 | // bits | 7| 6| 5| 4| 3| 2| 1| 0| 7| 6| 5| 4| 3| 2| 1| 0| 26 | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 27 | // | | Main Grp | Midd G | Sub Group | 28 | // +--+--------------------+-----------------------+ 29 | // +-----------------------------------------------+ 30 | // 16 bits | GROUP ADDRESS (2 level) | 31 | // +-----------------------+-----------------------+ 32 | // | OCTET 0 (high byte) | OCTET 1 (low byte) | 33 | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 34 | // bits | 7| 6| 5| 4| 3| 2| 1| 0| 7| 6| 5| 4| 3| 2| 1| 0| 35 | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 36 | // | | Main Grp | Sub Group | 37 | // +--+--------------------+-----------------------+ 38 | // NOTE: ets4 can utilise all 5 bits for the main group (0..31) 39 | 40 | const TYPE = { 41 | PHYSICAL: 0x00, 42 | GROUP: 0x01, 43 | }; 44 | 45 | const threeLevelPhysical = new Parser().bit4('l1').bit4('l2').uint8('l3'); 46 | const threeLevelGroup = new Parser().bit5('l1').bit3('l2').uint8('l3'); 47 | const twoLevel = new Parser().bit5('l1').bit11('l2'); 48 | 49 | // convert address stored in two-byte buffer to string 50 | const toString = function ( 51 | buf /*buffer*/, 52 | addrtype /*ADDRESS_TYPE*/, 53 | twoLevelAddressing /*boolean*/ 54 | ) { 55 | const group = addrtype == TYPE.GROUP; 56 | //KnxLog.get().trace('%j, type: %d, %j', buf, addrtype, knxnetprotocol.twoLevelAddressing); 57 | if (!Buffer.isBuffer(buf) || buf.length !== 2) 58 | throw 'not a buffer, or not a 2-byte address buffer'; 59 | if (group && twoLevelAddressing) { 60 | // 2 level group 61 | const { l1, l2 } = twoLevel.parse(buf); 62 | return [l1, l2].join('/'); 63 | } 64 | // 3 level physical or group address 65 | const sep = group ? '/' : '.'; 66 | const parser = group ? threeLevelGroup : threeLevelPhysical; 67 | const { l1, l2, l3 } = parser.parse(buf); 68 | return [l1, l2, l3].join(sep); 69 | }; 70 | 71 | // check for out of range integer 72 | const r = (x, max) => x < 0 || x > max; 73 | // parse address string to 2-byte Buffer 74 | const parse = function ( 75 | addr /*string*/, 76 | addrtype /*TYPE*/, 77 | twoLevelAddressing 78 | ) { 79 | if (!addr) { 80 | KnxLog.get().warn('Fix your code - no address given to Address.parse'); 81 | } 82 | const group = addrtype === TYPE.GROUP; 83 | const address = Buffer.allocUnsafe(2); 84 | const tokens = addr 85 | .split(group ? '/' : '.') 86 | .filter((w) => w.length > 0) 87 | .map((w) => parseInt(w)); 88 | if (tokens.length < 2) throw 'Invalid address (less than 2 tokens)'; 89 | const [hinibble, midnibble, lonibble] = tokens; 90 | if (group && twoLevelAddressing) { 91 | // 2 level group address 92 | if (r(hinibble, 31)) throw 'Invalid KNX 2-level main group: ' + addr; 93 | if (r(midnibble, 2047)) throw 'Invalid KNX 2-level sub group: ' + addr; 94 | address.writeUInt16BE((hinibble << 11) + midnibble, 0); 95 | return address; 96 | } 97 | if (tokens.length < 3) throw 'Invalid address - missing 3rd token'; 98 | if (group) { 99 | // 3 level group address 100 | if (r(hinibble, 31)) throw 'Invalid KNX 3-level main group: ' + addr; 101 | if (r(midnibble, 7)) throw 'Invalid KNX 3-level mid group: ' + addr; 102 | if (r(lonibble, 255)) throw 'Invalid KNX 3-level sub group: ' + addr; 103 | address.writeUInt8((hinibble << 3) + midnibble, 0); 104 | address.writeUInt8(lonibble, 1); 105 | return address; 106 | } 107 | // 3 level physical address 108 | if (r(hinibble, 15)) throw 'Invalid KNX area address: ' + addr; 109 | if (r(midnibble, 15)) throw 'Invalid KNX line address: ' + addr; 110 | if (r(lonibble, 255)) throw 'Invalid KNX device address: ' + addr; 111 | address.writeUInt8((hinibble << 4) + midnibble, 0); 112 | address.writeUInt8(lonibble, 1); 113 | return address; 114 | }; 115 | 116 | module.exports = { TYPE, toString, parse }; 117 | -------------------------------------------------------------------------------- /src/dptlib/dpt1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | const custom_truthiness = (value) => { 9 | const f = parseFloat(value); 10 | return !isNaN(f) && isFinite(f) 11 | ? // numeric values (in native and string form) are truthy if NOT zero 12 | f !== 0.0 13 | : // non-numeric value truthiness is Boolean true or the string 'true'. 14 | value === true || value === 'true'; 15 | }; 16 | 17 | exports.formatAPDU = (value) => Buffer.from([custom_truthiness(value)]); 18 | 19 | exports.fromBuffer = (buf) => { 20 | if (buf.length !== 1) 21 | return log.warn( 22 | 'DPT1.fromBuffer: buf should be 1 byte (got %d bytes)', 23 | buf.length 24 | ); 25 | return buf[0] !== 0; 26 | }; 27 | 28 | // DPT basetype info hash 29 | exports.basetype = { 30 | bitlength: 1, 31 | valuetype: 'basic', 32 | desc: '1-bit value', 33 | }; 34 | 35 | // DPT subtypes info hash 36 | exports.subtypes = { 37 | // 1.001 on/off 38 | '001': { 39 | use: 'G', 40 | name: 'DPT_Switch', 41 | desc: 'switch', 42 | enc: { 0: 'Off', 1: 'On' }, 43 | }, 44 | 45 | // 1.002 boolean 46 | '002': { 47 | use: 'G', 48 | name: 'DPT_Bool', 49 | desc: 'bool', 50 | enc: { 0: 'false', 1: 'true' }, 51 | }, 52 | 53 | // 1.003 enable 54 | '003': { 55 | use: 'G', 56 | name: 'DPT_Enable', 57 | desc: 'enable', 58 | enc: { 0: 'disable', 1: 'enable' }, 59 | }, 60 | 61 | // 1.004 ramp 62 | '004': { 63 | use: 'FB', 64 | name: 'DPT_Ramp', 65 | desc: 'ramp', 66 | enc: { 0: 'No ramp', 1: 'Ramp' }, 67 | }, 68 | 69 | // 1.005 alarm 70 | '005': { 71 | use: 'FB', 72 | name: 'DPT_Alarm', 73 | desc: 'alarm', 74 | enc: { 0: 'No alarm', 1: 'Alarm' }, 75 | }, 76 | 77 | // 1.006 binary value 78 | '006': { 79 | use: 'FB', 80 | name: 'DPT_BinaryValue', 81 | desc: 'binary value', 82 | enc: { 0: 'Low', 1: 'High' }, 83 | }, 84 | 85 | // 1.007 step 86 | '007': { 87 | use: 'FB', 88 | name: 'DPT_Step', 89 | desc: 'step', 90 | enc: { 0: 'Decrease', 1: 'Increase' }, 91 | }, 92 | 93 | // 1.008 up/down 94 | '008': { 95 | use: 'G', 96 | name: 'DPT_UpDown', 97 | desc: 'up/down', 98 | enc: { 0: 'Up', 1: 'Down' }, 99 | }, 100 | 101 | // 1.009 open/close 102 | '009': { 103 | use: 'G', 104 | name: 'DPT_OpenClose', 105 | desc: 'open/close', 106 | enc: { 0: 'Open', 1: 'Close' }, 107 | }, 108 | 109 | // 1.010 start/stop 110 | '010': { 111 | use: 'G', 112 | name: 'DPT_Start', 113 | desc: 'start/stop', 114 | enc: { 0: 'Stop', 1: 'Start' }, 115 | }, 116 | 117 | // 1.011 state 118 | '011': { 119 | use: 'FB', 120 | name: 'DPT_State', 121 | desc: 'state', 122 | enc: { 0: 'Inactive', 1: 'Active' }, 123 | }, 124 | 125 | // 1.012 invert 126 | '012': { 127 | use: 'FB', 128 | name: 'DPT_Invert', 129 | desc: 'invert', 130 | enc: { 0: 'Not inverted', 1: 'inverted' }, 131 | }, 132 | 133 | // 1.013 dim send style 134 | '013': { 135 | use: 'FB', 136 | name: 'DPT_DimSendStyle', 137 | desc: 'dim send style', 138 | enc: { 0: 'Start/stop', 1: 'Cyclically' }, 139 | }, 140 | 141 | // 1.014 input source 142 | '014': { 143 | use: 'FB', 144 | name: 'DPT_InputSource', 145 | desc: 'input source', 146 | enc: { 0: 'Fixed', 1: 'Calculated' }, 147 | }, 148 | 149 | // 1.015 reset 150 | '015': { 151 | use: 'G', 152 | name: 'DPT_Reset', 153 | desc: 'reset', 154 | enc: { 0: 'no action(dummy)', 1: 'reset command(trigger)' }, 155 | }, 156 | 157 | // 1.016 acknowledge 158 | '016': { 159 | use: 'G', 160 | name: 'DPT_Ack', 161 | desc: 'ack', 162 | enc: { 0: 'no action(dummy)', 1: 'acknowledge command(trigger)' }, 163 | }, 164 | 165 | // 1.017 trigger 166 | '017': { 167 | use: 'G', 168 | name: 'DPT_Trigger', 169 | desc: 'trigger', 170 | enc: { 0: 'trigger', 1: 'trigger' }, 171 | }, 172 | 173 | // 1.018 occupied 174 | '018': { 175 | use: 'G', 176 | name: 'DPT_Occupancy', 177 | desc: 'occupancy', 178 | enc: { 0: 'not occupied', 1: 'occupied' }, 179 | }, 180 | 181 | // 1.019 open window or door 182 | '019': { 183 | use: 'G', 184 | name: 'DPT_WindowDoor', 185 | desc: 'open window/door', 186 | enc: { 0: 'closed', 1: 'open' }, 187 | }, 188 | 189 | // 1.021 and/or 190 | '021': { 191 | use: 'FB', 192 | name: 'DPT_LogicalFunction', 193 | desc: 'and/or', 194 | enc: { 0: 'logical function OR', 1: 'logical function AND' }, 195 | }, 196 | 197 | // 1.022 scene A/B 198 | '022': { 199 | use: 'FB', 200 | name: 'DPT_Scene_AB', 201 | desc: 'scene A/B', 202 | enc: { 0: 'scene A', 1: 'scene B' }, 203 | }, 204 | 205 | // 1.023 shutter/blinds mode 206 | '023': { 207 | use: 'FB', 208 | name: 'DPT_ShutterBlinds_Mode', 209 | desc: 'shutter/blinds mode', 210 | enc: { 211 | 0: 'only move Up/Down mode (shutter)', 212 | 1: 'move Up/Down + StepStop mode (blind)', 213 | }, 214 | }, 215 | 216 | // 1.100 cooling/heating ---FIXME--- 217 | 100: { 218 | use: '???', 219 | name: 'DPT_Heat/Cool', 220 | desc: 'heat/cool', 221 | enc: { 0: '???', 1: '???' }, 222 | }, 223 | }; 224 | -------------------------------------------------------------------------------- /src/dptlib/dpt9.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const log = require('log-driver').logger; 7 | 8 | // 9 | // DPT9.*: 2-byte floating point value 10 | // 11 | 12 | const util = require('util'); 13 | // kudos to http://croquetweak.blogspot.gr/2014/08/deconstructing-floats-frexp-and-ldexp.html 14 | const ldexp = (mantissa, exponent) => 15 | exponent > 1023 // avoid multiplying by infinity 16 | ? mantissa * Math.pow(2, 1023) * Math.pow(2, exponent - 1023) 17 | : exponent < -1074 // avoid multiplying by zero 18 | ? mantissa * Math.pow(2, -1074) * Math.pow(2, exponent + 1074) 19 | : mantissa * Math.pow(2, exponent); 20 | 21 | const frexp = (value) => { 22 | if (value === 0) return [0, 0]; 23 | const data = new DataView(new ArrayBuffer(8)); 24 | data.setFloat64(0, value); 25 | let bits = (data.getUint32(0) >>> 20) & 0x7ff; 26 | if (bits === 0) { 27 | data.setFloat64(0, value * Math.pow(2, 64)); 28 | bits = ((data.getUint32(0) >>> 20) & 0x7ff) - 64; 29 | } 30 | const exponent = bits - 1022; 31 | const mantissa = ldexp(value, -exponent); 32 | return [mantissa, exponent]; 33 | }; 34 | 35 | exports.formatAPDU = (value) => { 36 | if (!isFinite(value)) 37 | return log.warn('DPT9: cannot write non-numeric or undefined value'); 38 | 39 | const arr = frexp(value); 40 | const [mantissa, exponent] = arr; 41 | // find the minimum exponent that will upsize the normalized mantissa (0,5 to 1 range) 42 | // in order to fit in 11 bits ([-2048, 2047]) 43 | let max_mantissa = 0; 44 | for (e = exponent; e >= -15; e--) { 45 | max_mantissa = ldexp(100 * mantissa, e); 46 | if (max_mantissa > -2048 && max_mantissa < 2047) break; 47 | } 48 | const sign = mantissa < 0 ? 1 : 0; 49 | const mant = mantissa < 0 ? ~(max_mantissa ^ 2047) : max_mantissa; 50 | const exp = exponent - e; 51 | // yucks 52 | return Buffer.from([(sign << 7) + (exp << 3) + (mant >> 8), mant % 256]); 53 | }; 54 | 55 | exports.fromBuffer = (buf) => { 56 | if (buf.length != 2) 57 | return log.warn( 58 | 'DPT9.fromBuffer: buf should be 2 bytes long (got %d bytes)', 59 | buf.length 60 | ); 61 | 62 | const sign = buf[0] >> 7; 63 | const exponent = (buf[0] & 0b01111000) >> 3; 64 | let mantissa = 256 * (buf[0] & 0b00000111) + buf[1]; 65 | if (sign) mantissa = ~(mantissa ^ 2047); 66 | return parseFloat(ldexp(0.01 * mantissa, exponent).toPrecision(15)); 67 | }; 68 | 69 | // DPT9 basetype info 70 | exports.basetype = { 71 | bitlength: 16, 72 | valuetype: 'basic', 73 | desc: '16-bit floating point value', 74 | }; 75 | 76 | // DPT9 subtypes 77 | exports.subtypes = { 78 | // 9.001 temperature (oC) 79 | '001': { 80 | name: 'DPT_Value_Temp', 81 | desc: 'temperature', 82 | unit: '°C', 83 | range: [-273, 670760], 84 | }, 85 | 86 | // 9.002 temperature difference (oC) 87 | '002': { 88 | name: 'DPT_Value_Tempd', 89 | desc: 'temperature difference', 90 | unit: '°C', 91 | range: [-670760, 670760], 92 | }, 93 | 94 | // 9.003 kelvin/hour (K/h) 95 | '003': { 96 | name: 'DPT_Value_Tempa', 97 | desc: 'kelvin/hour', 98 | unit: '°K/h', 99 | range: [-670760, 670760], 100 | }, 101 | 102 | // 9.004 lux (Lux) 103 | '004': { 104 | name: 'DPT_Value_Lux', 105 | desc: 'lux', 106 | unit: 'lux', 107 | range: [0, 670760], 108 | }, 109 | 110 | // 9.005 speed (m/s) 111 | '005': { 112 | name: 'DPT_Value_Wsp', 113 | desc: 'wind speed', 114 | unit: 'm/s', 115 | range: [0, 670760], 116 | }, 117 | 118 | // 9.006 pressure (Pa) 119 | '006': { 120 | name: 'DPT_Value_Pres', 121 | desc: 'pressure', 122 | unit: 'Pa', 123 | range: [0, 670760], 124 | }, 125 | 126 | // 9.007 humidity (%) 127 | '007': { 128 | name: 'DPT_Value_Humidity', 129 | desc: 'humidity', 130 | unit: '%', 131 | range: [0, 670760], 132 | }, 133 | 134 | // 9.008 parts/million (ppm) 135 | '008': { 136 | name: 'DPT_Value_AirQuality', 137 | desc: 'air quality', 138 | unit: 'ppm', 139 | range: [0, 670760], 140 | }, 141 | 142 | // 9.010 time (s) 143 | '010': { 144 | name: 'DPT_Value_Time1', 145 | desc: 'time(sec)', 146 | unit: 's', 147 | range: [-670760, 670760], 148 | }, 149 | 150 | // 9.011 time (ms) 151 | '011': { 152 | name: 'DPT_Value_Time2', 153 | desc: 'time(msec)', 154 | unit: 'ms', 155 | range: [-670760, 670760], 156 | }, 157 | 158 | // 9.020 voltage (mV) 159 | '020': { 160 | name: 'DPT_Value_Volt', 161 | desc: 'voltage', 162 | unit: 'mV', 163 | range: [-670760, 670760], 164 | }, 165 | 166 | // 9.021 current (mA) 167 | '021': { 168 | name: 'DPT_Value_Curr', 169 | desc: 'current', 170 | unit: 'mA', 171 | range: [-670760, 670760], 172 | }, 173 | 174 | // 9.022 power density (W/m2) 175 | '022': { 176 | name: 'DPT_PowerDensity', 177 | desc: 'power density', 178 | unit: 'W/m²', 179 | range: [-670760, 670760], 180 | }, 181 | 182 | // 9.023 kelvin/percent (K/%) 183 | '023': { 184 | name: 'DPT_KelvinPerPercent', 185 | desc: 'Kelvin / %', 186 | unit: 'K/%', 187 | range: [-670760, 670760], 188 | }, 189 | 190 | // 9.024 power (kW) 191 | '024': { 192 | name: 'DPT_Power', 193 | desc: 'power (kW)', 194 | unit: 'kW', 195 | range: [-670760, 670760], 196 | }, 197 | 198 | // 9.025 volume flow (l/h) 199 | '025': { 200 | name: 'DPT_Value_Volume_Flow', 201 | desc: 'volume flow', 202 | unit: 'l/h', 203 | range: [-670760, 670760], 204 | }, 205 | 206 | // 9.026 rain amount (l/m2) 207 | '026': { 208 | name: 'DPT_Rain_Amount', 209 | desc: 'rain amount', 210 | unit: 'l/m²', 211 | range: [-670760, 670760], 212 | }, 213 | 214 | // 9.027 temperature (Fahrenheit) 215 | '027': { 216 | name: 'DPT_Value_Temp_F', 217 | desc: 'temperature (F)', 218 | unit: '°F', 219 | range: -[459.6, 670760], 220 | }, 221 | 222 | // 9.028 wind speed (km/h) 223 | '028': { 224 | name: 'DPT_Value_Wsp_kmh', 225 | desc: 'wind speed (km/h)', 226 | unit: 'km/h', 227 | range: [0, 670760], 228 | }, 229 | }; 230 | -------------------------------------------------------------------------------- /README-API.md: -------------------------------------------------------------------------------- 1 | ## Connect to your KNX IP router 2 | 3 | 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. 4 | 5 | 6 | ```js 7 | var connection = new knx.Connection( { 8 | // ip address and port of the KNX router or interface 9 | ipAddr: '127.0.0.1', ipPort: 3671, 10 | // in case you need to specify the multicast interface (say if you have more than one) 11 | interface: 'eth0', 12 | // the KNX physical address we'd like to use 13 | physAddr: '15.15.15', 14 | // set the log level for messsages printed on the console. This can be 'error', 'warn', 'info' (default), 'debug', or 'trace'. 15 | loglevel: 'info', 16 | // do not automatically connect, but use connection.Connect() to establish connection 17 | manualConnect: true, 18 | // use tunneling with multicast (router) - this is NOT supported by all routers! See README-resilience.md 19 | forceTunneling: true, 20 | // wait at least 10 millisec between each datagram 21 | minimumDelay: 10, 22 | // enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this 23 | suppress_ack_ldatareq: false, 24 | // 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. 25 | localEchoInTunneling:false, 26 | // define your event handlers here: 27 | handlers: { 28 | // wait for connection establishment before sending anything! 29 | connected: function() { 30 | console.log('Hurray, I can talk KNX!'); 31 | // WRITE an arbitrary boolean request to a DPT1 group address 32 | connection.write("1/0/0", 1); 33 | // you also WRITE to an explicit datapoint type, eg. DPT9.001 is temperature Celcius 34 | connection.write("2/1/0", 22.5, "DPT9.001"); 35 | // you can also issue a READ request and pass a callback to capture the response 36 | connection.read("1/0/1", (src, responsevalue) => { ... }); 37 | }, 38 | // get notified for all KNX events: 39 | event: function(evt, src, dest, value) { console.log( 40 | "event: %s, src: %j, dest: %j, value: %j", 41 | evt, src, dest, value 42 | ); 43 | }, 44 | // get notified on connection errors 45 | error: function(connstatus) { 46 | console.log("**** ERROR: %j", connstatus); 47 | } 48 | } 49 | }); 50 | ``` 51 | 52 | **Important**: connection.write() will only accept *raw APDU payloads* and a DPT. 53 | This practically means that for *reading and writing to anything other than a binary 54 | switch* (eg. for dimmer controls) you'll need to declare one or more *datapoints*. 55 | 56 | ### Declare datapoints based on their DPT 57 | 58 | Datapoints correlate an *endpoint* (identifed by a group address such as '1/2/3') 59 | with a *DPT* (DataPoint Type), so that *serialization* of values to and from KNX 60 | works correctly (eg. temperatures as 16bit floats), and values are being translated 61 | to Javascript objects and back. 62 | 63 | ```js 64 | // declare a simple binary control datapoint 65 | var binary_control = new knx.Datapoint({ga: '1/0/1', dpt: 'DPT1.001'}); 66 | // bind it to the active connection 67 | binary_control.bind(connection); 68 | // write a new value to the bus 69 | binary_control.write(true); // or false! 70 | // send a read request, and fire the callback upon response 71 | binary_control.read( function (response) { 72 | console.log("KNX response: %j", response); 73 | }; 74 | // or declare a dimmer control 75 | var dimmer_control = new knx.Datapoint({ga: '1/2/33', dpt: 'DPT3.007'}); 76 | // declare a binary STATUS datapoint, which will automatically read off its value 77 | var binary_status = new knx.Datapoint({ga: '1/0/1', dpt: 'DPT1.001', autoread: true}); 78 | ``` 79 | 80 | Datapoints need to be bound to a connection. This can be done either at their 81 | creation, *or* using their `bind()` call. Its important to highlight that before 82 | you start defining datapoints (and devices as we'll see later), your code 83 | *needs to ensure that the connection has been established*, usually by declaring them in the 'connected' handler: 84 | 85 | ```js 86 | var connection = knx.Connection({ 87 | handlers: { 88 | connected: function() { 89 | console.log('----------'); 90 | console.log('Connected!'); 91 | console.log('----------'); 92 | var dp = new knx.Datapoint({ga: '1/1/1'}, connection); 93 | // Now send off a couple of requests: 94 | dp.read((src, value) => { 95 | console.log("**** RESPONSE %j reports current value: %j", src, value); 96 | }); 97 | dp.write(1); 98 | } 99 | } 100 | }); 101 | ``` 102 | 103 | ### Declare your devices 104 | 105 | You can define a device (basically a set of GA's that are related to a 106 | physical KNX device eg. a binary switch) so that you have higher level of control: 107 | 108 | ```js 109 | var light = new knx.Devices.BinarySwitch({ga: '1/1/8', status_ga: '1/1/108'}, connection); 110 | console.log("The current light status is %j", light.status.current_value); 111 | light.control.on('change', function(oldvalue, newvalue) { 112 | console.log("**** LIGHT control changed from: %j to: %j", oldvalue, newvalue); 113 | }); 114 | light.status.on('change', function(oldvalue, newvalue) { 115 | console.log("**** LIGHT status changed from: %j to: %j", oldvalue, newvalue); 116 | }); 117 | light.switchOn(); // or switchOff(); 118 | ``` 119 | 120 | This effectively creates a pair of datapoints typically associated with a binary 121 | switch, one for controlling it and another for getting a status feedback (eg via 122 | manual operation) 123 | 124 | ### Write raw buffers 125 | 126 | If you encode the values by yourself, you can write raw buffers with `writeRaw(groupaddress: string, buffer: Buffer, bitlength?: Number, callback?: () => void)`. 127 | 128 | The third (optional) parameter `bitlength` is necessary for datapoint types 129 | where the bitlength does not equal the buffers bytelength * 8. 130 | This is the case for dpt 1 (bitlength 1), 2 (bitlength 2) and 3 (bitlength 4). 131 | For other dpts the paramter can be omitted. 132 | 133 | ```js 134 | // Write raw buffer to a groupaddress with dpt 1 (e.g light on = value true = Buffer<01>) with a bitlength of 1 135 | connection.writeRaw('1/0/0', Buffer.from('01', 'hex'), 1) 136 | // Write raw buffer to a groupaddress with dpt 9 (e.g temperature 18.4 °C = Buffer<0730>) without bitlength 137 | connection.writeRaw('1/0/0', Buffer.from('0730', 'hex')) 138 | ``` 139 | 140 | ### Disconnect 141 | 142 | In order to cleanly disconnect, you must send the Disconnect-Request and give the KNX-IP-Stack enough time to receive the Disconnect-Response back from the IP Gateway. Most IP-Gateways will have a timeout and clean stale connection up, even if you do not disconect cleanly, but depending on the limits on the number of parallel active connections, this will limit your ability to re-connect until the timeout has passed. 143 | 144 | For NodeJS cleaning up when the script exits, this requires something like [async-exit-hook](https://www.npmjs.com/package/async-exit-hook): 145 | 146 | ```js 147 | const exitHook = require('async-exit-hook'); 148 | 149 | exitHook(cb => { 150 | console.log('Disconnecting from KNX…'); 151 | connection.Disconnect(() => { 152 | console.log('Disconnected from KNX'); 153 | cb(); 154 | }); 155 | }); 156 | ``` 157 | -------------------------------------------------------------------------------- /src/dptlib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | /* 7 | Datatypes 8 | ========= 9 | KNX/EIB Function Information length EIS DPT Value 10 | Switch 1 Bit EIS 1 DPT 1 0,1 11 | Dimming (Position, Control, Value) 1 Bit, 4 Bit, 8 Bit EIS 2 DPT 3 [0,0]...[1,7] 12 | Time 3 Byte EIS 3 DPT 10 13 | Date 3 Byte EIS 4 DPT 11 14 | Floating point 2 Byte EIS 5 DPT 9 -671088,64 - 670760,96 15 | 8-bit unsigned value 1 Byte EIS 6 DPT 5 0...255 16 | 8-bit unsigned value 1 Byte DPT 5.001 DPT 5.001 0...100 17 | Blinds / Roller shutter 1 Bit EIS 7 DPT 1 0,1 18 | Priority 2 Bit EIS 8 DPT 2 [0,0]...[1,1] 19 | IEEE Floating point 4 Byte EIS 9 DPT 14 4-Octet Float Value IEEE 754 20 | 16-bit unsigned value 2 Byte EIS 10 DPT 7 0...65535 21 | 16-bit signed value 2 Byte DPT 8 DPT 8 -32768...32767 22 | 32-bit unsigned value 4 Byte EIS 11 DPT 12 0...4294967295 23 | 32-bit signed value 4 Byte DPT 13 DPT 13 -2147483648...2147483647 24 | Access control 1 Byte EIS 12 DPT 15 25 | ASCII character 1 Byte EIS 13 DPT 4 26 | 8859_1 character 1 Byte DPT 4.002 DPT 4.002 27 | 8-bit signed value 1 Byte EIS 14 DPT 6 -128...127 28 | 14 character ASCII 14 Byte EIS 15 DPT 16 29 | 14 character 8859_1 14 Byte DPT 16.001 DPT 16.001 30 | Scene 1 Byte DPT 17 DPT 17 0...63 31 | HVAC 1 Byte DPT 20 DPT 20 0..255 32 | Unlimited string 8859_1 . DPT 24 DPT 24 33 | List 3-byte value 3 Byte DPT 232 DPT 232 RGB[0,0,0]...[255,255,255] 34 | */ 35 | 36 | const fs = require('fs'); 37 | const path = require('path'); 38 | const util = require('util'); 39 | const log = require('log-driver').logger; 40 | 41 | const dpts = {}; 42 | for (const entry of fs.readdirSync(__dirname)) { 43 | const matches = entry.match(/(dpt.*)\.js/); 44 | if (!matches) continue; 45 | const dptid = matches[1].toUpperCase(); // DPT1..DPTxxx 46 | const mod = require(__dirname + path.sep + entry); 47 | if ( 48 | !mod.hasOwnProperty('basetype') || 49 | !mod.basetype.hasOwnProperty('bitlength') 50 | ) 51 | throw 'incomplete ' + dptid + ', missing basetype and/or bitlength!'; 52 | mod.id = dptid; 53 | dpts[dptid] = mod; 54 | //log.trace('DPT library: loaded %s (%s)', dptid, dpts[dptid].basetype.desc); 55 | } 56 | 57 | // a generic DPT resolution function 58 | // DPTs might come in as 9/"9"/"9.001"/"DPT9.001" 59 | dpts.resolve = (dptid) => { 60 | const m = dptid 61 | .toString() 62 | .toUpperCase() 63 | .match(/^(?:DPT)?(\d+)(\.(\d+))?$/); 64 | if (m === null) throw 'Invalid DPT format: ' + dptid; 65 | 66 | const dpt = dpts[util.format('DPT%s', m[1])]; 67 | if (!dpt) throw 'Unsupported DPT: ' + dptid; 68 | 69 | const cloned_dpt = cloneDpt(dpt); 70 | if (m[3]) { 71 | cloned_dpt.subtypeid = m[3]; 72 | cloned_dpt.subtype = cloned_dpt.subtypes[m[3]]; 73 | } 74 | 75 | return cloned_dpt; 76 | }; 77 | 78 | /* POPULATE an APDU object from a given Javascript value for the given DPT 79 | * - either by a custom DPT formatAPDU function 80 | * - or by this generic version, which: 81 | * -- 1) checks if the value adheres to the range set from the DPT's bitlength 82 | * 83 | */ 84 | dpts.populateAPDU = (value, apdu, dptid) => { 85 | const dpt = dpts.resolve(dptid || 'DPT1'); 86 | const nbytes = Math.ceil(dpt.basetype.bitlength / 8); 87 | apdu.data = Buffer.alloc(nbytes); 88 | apdu.bitlength = (dpt.basetype && dpt.basetype.bitlength) || 1; 89 | let tgtvalue = value; 90 | // get the raw APDU data for the given JS value 91 | if (typeof dpt.formatAPDU == 'function') { 92 | // nothing to do here, DPT-specific formatAPDU implementation will handle everything 93 | // log.trace('>>> custom formatAPDU(%s): %j', dptid, value); 94 | apdu.data = dpt.formatAPDU(value); 95 | // log.trace('<<< custom formatAPDU(%s): %j', dptid, apdu.data); 96 | return apdu; 97 | } 98 | 99 | if (!isFinite(value)) 100 | throw util.format('Invalid value, expected a %s', dpt.desc); 101 | // check if value is in range, be it explicitly defined or implied from bitlength 102 | const [r_min, r_max] = dpt.basetype.hasOwnProperty('range') 103 | ? dpt.basetype.range 104 | : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; // TODO: Maybe bitshift instead of pow? 105 | // is there a scalar range? eg. DPT5.003 angle degrees (0=0, ff=360) 106 | if ( 107 | dpt.hasOwnProperty('subtype') && 108 | dpt.subtype.hasOwnProperty('scalar_range') 109 | ) { 110 | const [s_min, s_max] = dpt.subtype.scalar_range; 111 | if (value < s_min || value > s_max) { 112 | log.trace( 113 | 'Value %j(%s) out of scalar range(%j) for %s', 114 | value, 115 | typeof value, 116 | scalar, 117 | dpt.id 118 | ); 119 | } else { 120 | // convert value from its scalar representation 121 | // e.g. in DPT5.001, 50(%) => 0x7F , 100(%) => 0xFF 122 | const a = (s_max - s_min) / (r_max - r_min); 123 | const b = s_min - r_min; 124 | tgtvalue = Math.round((value - b) / a); 125 | } 126 | } 127 | // just a plain numeric value, only check if within bounds 128 | else if (value < r_min || value > r_max) { 129 | log.trace( 130 | 'Value %j(%s) out of bounds(%j) for %s.%s', 131 | value, 132 | typeof value, 133 | range, 134 | dpt.id, 135 | dpt.subtypeid 136 | ); 137 | } 138 | 139 | // generic APDU is assumed to convey an unsigned integer of arbitrary bitlength 140 | if ( 141 | dpt.basetype.hasOwnProperty('signedness') && 142 | dpt.basetype.signedness == 'signed' 143 | ) { 144 | apdu.data.writeIntBE(tgtvalue, 0, nbytes); 145 | } else { 146 | apdu.data.writeUIntBE(tgtvalue, 0, nbytes); 147 | } 148 | 149 | // log.trace('generic populateAPDU tgtvalue=%j(%s) nbytes=%d => apdu=%j', tgtvalue, typeof tgtvalue, nbytes, apdu); 150 | }; 151 | 152 | /* get the correct Javascript value from a APDU buffer for the given DPT 153 | * - either by a custom DPT formatAPDU function 154 | * - or by this generic version, which: 155 | * -- 1) checks if the value adheres to the range set from the DPT's bitlength 156 | */ 157 | dpts.fromBuffer = (buf, dpt) => { 158 | // sanity check 159 | if (!dpt) throw util.format('DPT %s not found', dpt); 160 | // get the raw APDU data for the given JS value 161 | if (typeof dpt.fromBuffer == 'function') { 162 | // nothing to do here, DPT-specific fromBuffer implementation will handle everything 163 | return dpt.fromBuffer(buf); 164 | } 165 | // log.trace('%s buflength == %d => %j', typeof buf, buf.length, JSON.stringify(buf) ); 166 | if (buf.length > 6) { 167 | throw 'cannot handle unsigned integers more then 6 bytes in length'; 168 | } 169 | let value = 0; 170 | if ( 171 | dpt.basetype.hasOwnProperty('signedness') && 172 | dpt.basetype.signedness == 'signed' 173 | ) 174 | value = buf.readIntBE(0, buf.length); 175 | else value = buf.readUIntBE(0, buf.length); 176 | 177 | // log.trace(' ../knx/src/index.js : DPT : ' + JSON.stringify(dpt)); // for exploring dpt and implementing description 178 | if ( 179 | dpt.hasOwnProperty('subtype') && 180 | dpt.subtype.hasOwnProperty('scalar_range') 181 | ) { 182 | const [r_min, r_max] = dpt.basetype.hasOwnProperty('range') 183 | ? dpt.basetype.range 184 | : [0, Math.pow(2, dpt.basetype.bitlength) - 1]; 185 | const [s_min, s_max] = dpt.subtype.scalar_range; 186 | // convert value from its scalar representation 187 | // e.g. in DPT5.001, 50(%) => 0x7F , 100(%) => 0xFF 188 | const a = (s_max - s_min) / (r_max - r_min); 189 | const b = s_min - r_min; 190 | value = Math.round(a * value + b); 191 | //log.trace('fromBuffer scalar a=%j b=%j %j', a,b, value); 192 | } 193 | 194 | // log.trace('generic fromBuffer buf=%j, value=%j', buf, value); 195 | return value; 196 | }; 197 | 198 | const cloneDpt = (d) => { 199 | const { fromBuffer, formatAPDU } = d; 200 | return { ...JSON.parse(JSON.stringify(d)), fromBuffer, formatAPDU }; 201 | }; 202 | 203 | module.exports = dpts; 204 | -------------------------------------------------------------------------------- /test/knxproto/test-proto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const knxnetprotocol = require('../../src/KnxProtocol.js'); 7 | const assert = require('assert'); 8 | const test = require('tape'); 9 | knxnetprotocol.debug = true; 10 | 11 | // 12 | test('KNX protocol unmarshaller', function(t) { 13 | var tests = { 14 | "ETS5 programming request": new Buffer([ 15 | 6, 16, 4, 32, 0, 20, 4, 34, 16 | 1, 0, 197, 0, 17, 252, 17, 253, 17 | 17, 1, 0, 238 18 | ]) 19 | }; 20 | Object.keys(tests).forEach((key, idx) => { 21 | var buf = tests[key]; 22 | // unmarshal from a buffer... 23 | var reader = knxnetprotocol.createReader(buf); 24 | var writer = knxnetprotocol.createWriter(); 25 | reader.KNXNetHeader('tmp'); 26 | var decoded = reader.next()['tmp']; 27 | console.log("\n=== %s: %j ===> %j", key, buf, decoded); 28 | t.ok(decoded != undefined, `${key}: unmarshaled KNX datagram`); 29 | }); 30 | t.end(); 31 | }); 32 | 33 | test('KNX protocol marshal+unmarshal', function(t) { 34 | var tests = { 35 | CONNECT_REQUEST: new Buffer( 36 | "06100205001a0801c0a80ab3d96d0801c0a80ab3d83604040200", 'hex'), 37 | CONNECT_RESPONSE: new Buffer( 38 | "061002060014030008010a0c17350e5704040000", 'hex'), 39 | "CONNECT_RESPONSE, failure E_NO_MORE_CONNECTIONS: 0x24": new Buffer( 40 | "0610020600080024", 'hex'), 41 | "tunneling request (GroupValue_Read) apdu=1byte": new Buffer( 42 | "061004200015040200002e00bce000000832010000", 'hex'), 43 | "tunneling request (GroupValue_Write) apdu=1byte": new Buffer( 44 | "061004200015040200002e00bce000000832010081", 'hex'), 45 | "tunneling request (GroupValue_Write) apdu=2byte": new Buffer( 46 | "061004200016040201002900bce00000083b0200804a", 'hex'), 47 | "routing indication": new Buffer( 48 | "0610053000112900bce0ff0f0908010000", 'hex'), 49 | DISCONNECT_REQUEST: new Buffer([ 50 | 6, 16, 2, 9, 0, 16, 142, 142, 51 | 8, 1, 192, 168, 2, 222, 14, 87 52 | ]), 53 | }; 54 | Object.keys(tests).forEach((key, idx) => { 55 | var buf = tests[key]; 56 | // unmarshal from a buffer... 57 | var reader = knxnetprotocol.createReader(buf); 58 | var writer = knxnetprotocol.createWriter(); 59 | reader.KNXNetHeader('tmp'); 60 | var decoded = reader.next()['tmp']; 61 | console.log("\n=== %s: %j ===> %j", key, buf, decoded); 62 | t.ok(decoded != undefined, `${key}: unmarshaled KNX datagram`); 63 | // then marshal the datagram again into a buffer... 64 | writer.KNXNetHeader(decoded); 65 | if (Buffer.compare(buf, writer.buffer) != 0) { 66 | console.log("\n\n========\n FAIL: %s\n========\nbuffer is different:\n", key); 67 | 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') 68 | console.log('expected : %s', buf.toString('hex')); 69 | console.log('got instead: %s', writer.buffer.toString('hex')); 70 | } 71 | t.ok(Buffer.compare(buf, writer.buffer) == 0); 72 | }); 73 | t.end(); 74 | }); 75 | 76 | test('KNX protocol marshaller', function(t) { 77 | var tests = { 78 | "compose tunneling request (write) apdu=1byte - turn ON a light": { 79 | hexbuf: "061004200015040200002e00bce000000832010081", 80 | dgram: { 81 | header_length: 6, 82 | protocol_version: 16, 83 | service_type: 1056, 84 | total_length: 21, 85 | tunnstate: { 86 | header_length: 4, 87 | channel_id: 2, 88 | seqnum: 0, 89 | rsvd: 0 90 | }, 91 | cemi: { 92 | msgcode: 46, 93 | addinfo_length: 0, 94 | ctrl: { 95 | frameType: 1, 96 | reserved: 0, 97 | repeat: 1, 98 | broadcast: 1, 99 | priority: 3, 100 | acknowledge: 0, 101 | confirm: 0, 102 | destAddrType: 1, 103 | hopCount: 6, 104 | extendedFrame: 0 105 | }, 106 | src_addr: '0.0.0', 107 | dest_addr: '1/0/50', 108 | apdu: { 109 | tpci: 0, 110 | apci: 'GroupValue_Write', 111 | data: 1 112 | } 113 | } 114 | } 115 | }, 116 | 117 | "compose tunneling request (write) apdu=1byte - turn OFF a light": { 118 | hexbuf: "061004200015040200002e00bce000000832010080", 119 | dgram: { 120 | header_length: 6, 121 | protocol_version: 16, 122 | service_type: 1056, 123 | total_length: 21, 124 | tunnstate: { 125 | header_length: 4, 126 | channel_id: 2, 127 | seqnum: 0, 128 | rsvd: 0 129 | }, 130 | cemi: { 131 | msgcode: 46, 132 | addinfo_length: 0, 133 | ctrl: { 134 | frameType: 1, 135 | reserved: 0, 136 | repeat: 1, 137 | broadcast: 1, 138 | priority: 3, 139 | acknowledge: 0, 140 | confirm: 0, 141 | destAddrType: 1, 142 | hopCount: 6, 143 | extendedFrame: 0 144 | }, 145 | src_addr: '0.0.0', 146 | dest_addr: '1/0/50', 147 | apdu: { 148 | tpci: 0, 149 | apci: 'GroupValue_Write', 150 | data: [0] 151 | } 152 | } 153 | } 154 | }, 155 | 156 | "compose tunneling request (write) apdu=2byte - DIMMING a light to 10%": { 157 | hexbuf: "061004200016040200002e00bce0000008320200800a", 158 | dgram: { 159 | header_length: 6, 160 | protocol_version: 16, 161 | service_type: 1056, 162 | total_length: 21, 163 | tunnstate: { 164 | header_length: 4, 165 | channel_id: 2, 166 | seqnum: 0, 167 | rsvd: 0 168 | }, 169 | cemi: { 170 | msgcode: 46, 171 | addinfo_length: 0, 172 | ctrl: { 173 | frameType: 1, 174 | reserved: 0, 175 | repeat: 1, 176 | broadcast: 1, 177 | priority: 3, 178 | acknowledge: 0, 179 | confirm: 0, 180 | destAddrType: 1, 181 | hopCount: 6, 182 | extendedFrame: 0 183 | }, 184 | src_addr: '0.0.0', 185 | dest_addr: '1/0/50', 186 | apdu: { 187 | bitlength: 8, 188 | tpci: 0, 189 | apci: 'GroupValue_Write', 190 | data: [10] 191 | } 192 | } 193 | } 194 | }, 195 | 196 | "temperature response, apdu=2-byte": { 197 | hexbuf: "061004200017040200002e00BCD0110B000F0300400730", 198 | dgram: { 199 | header_length: 6, 200 | protocol_version: 16, 201 | service_type: 1056, 202 | total_length: 22, 203 | tunnstate: { 204 | header_length: 4, 205 | channel_id: 2, 206 | seqnum: 0, 207 | rsvd: 0 208 | }, 209 | cemi: { 210 | msgcode: 46, 211 | addinfo_length: 0, 212 | ctrl: { 213 | frameType: 1, 214 | reserved: 0, 215 | repeat: 1, 216 | broadcast: 1, 217 | priority: 3, 218 | acknowledge: 0, 219 | confirm: 0, 220 | destAddrType: 1, 221 | hopCount: 5, 222 | extendedFrame: 0 223 | }, 224 | src_addr: '1.1.11', 225 | dest_addr: '0/0/15', 226 | apdu: { 227 | bitlength: 16, 228 | tpci: 0, 229 | apci: 'GroupValue_Response', 230 | data: [0x07, 0x30] 231 | } 232 | } 233 | } 234 | }, 235 | } 236 | 237 | 238 | Object.keys(tests).forEach((key, idx) => { 239 | var testcase = tests[key]; 240 | var buf = typeof testcase.hexbuf == 'string' ? 241 | new Buffer(testcase.hexbuf.replace(/\s/g, ''), 'hex') : hexbuf; 242 | console.log("\n=== %s", key); 243 | // marshal the test datagram 244 | var writer = knxnetprotocol.createWriter(); 245 | writer.KNXNetHeader(testcase.dgram); 246 | if (Buffer.compare(buf, writer.buffer) != 0) { 247 | // if this fails, unmarshal the buffer again to a datagram 248 | var reader = knxnetprotocol.createReader(writer.buffer); 249 | reader.KNXNetHeader('tmp'); 250 | var decoded = reader.next()['tmp']; 251 | console.log("\n\n========\n FAIL: %s\n========\nbuffer is different:\n", key); 252 | 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') 253 | console.log('expected : %s', buf.toString('hex')); 254 | console.log('got instead: %s', writer.buffer.toString('hex')); 255 | } 256 | t.ok(Buffer.compare(buf, writer.buffer) == 0); 257 | }); 258 | t.end(); 259 | }); 260 | -------------------------------------------------------------------------------- /src/Connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const util = require('util'); 7 | 8 | const FSM = require('./FSM'); 9 | const DPTLib = require('./dptlib'); 10 | const KnxLog = require('./KnxLog'); 11 | const KnxConstants = require('./KnxConstants'); 12 | const KnxNetProtocol = require('./KnxProtocol'); 13 | 14 | // bind incoming UDP packet handler 15 | FSM.prototype.onUdpSocketMessage = function(msg, rinfo, callback) { 16 | // get the incoming packet's service type ... 17 | try { 18 | const reader = KnxNetProtocol.createReader(msg); 19 | reader.KNXNetHeader('tmp'); 20 | const dg = reader.next()['tmp']; 21 | const descr = datagramDesc(dg); 22 | KnxLog.get().trace('(%s): Received %s message: %j', this.compositeState(), descr, dg); 23 | if (!isNaN(this.channel_id) && 24 | ((dg.hasOwnProperty('connstate') && 25 | dg.connstate.channel_id != this.channel_id) || 26 | (dg.hasOwnProperty('tunnstate') && 27 | dg.tunnstate.channel_id != this.channel_id))) { 28 | KnxLog.get().trace('(%s): *** Ignoring %s datagram for other channel (own: %d)', 29 | this.compositeState(), descr, this.channel_id); 30 | } else { 31 | // ... to drive the state machine (eg "inbound_TUNNELING_REQUEST_L_Data.ind") 32 | const signal = util.format('inbound_%s', descr); 33 | if (descr === "DISCONNECT_REQUEST") { 34 | KnxLog.get().info("empty internal fsm queue due to %s: ", signal); 35 | this.clearQueue(); 36 | } 37 | this.handle(signal, dg); 38 | } 39 | } catch(err) { 40 | KnxLog.get().debug('(%s): Incomplete/unparseable UDP packet: %s: %s', 41 | this.compositeState(),err, msg.toString('hex') 42 | ); 43 | } 44 | }; 45 | 46 | FSM.prototype.AddConnState = function(datagram) { 47 | datagram.connstate = { 48 | channel_id: this.channel_id, 49 | state: 0 50 | } 51 | } 52 | 53 | FSM.prototype.AddTunnState = function(datagram) { 54 | // add the remote IP router's endpoint 55 | datagram.tunnstate = { 56 | channel_id: this.channel_id, 57 | tunnel_endpoint: this.remoteEndpoint.addr + ':' + this.remoteEndpoint.port 58 | } 59 | } 60 | 61 | const AddCRI = (datagram) => { 62 | // add the CRI 63 | datagram.cri = { 64 | connection_type: KnxConstants.CONNECTION_TYPE.TUNNEL_CONNECTION, 65 | knx_layer: KnxConstants.KNX_LAYER.LINK_LAYER, 66 | unused: 0 67 | } 68 | } 69 | 70 | FSM.prototype.AddCEMI = function(datagram, msgcode) { 71 | const sendAck = ((msgcode || 0x11) == 0x11) && !this.options.suppress_ack_ldatareq; // only for L_Data.req 72 | datagram.cemi = { 73 | msgcode: msgcode || 0x11, // default: L_Data.req for tunneling 74 | ctrl: { 75 | frameType: 1, // 0=extended 1=standard 76 | reserved: 0, // always 0 77 | repeat: 1, // the OPPOSITE: 1=do NOT repeat 78 | broadcast: 1, // 0-system broadcast 1-broadcast 79 | priority: 3, // 0-system 1-normal 2-urgent 3-low 80 | acknowledge: sendAck ? 1 : 0, 81 | confirm: 0, // FIXME: only for L_Data.con 0-ok 1-error 82 | // 2nd byte 83 | destAddrType: 1, // FIXME: 0-physical 1-groupaddr 84 | hopCount: 6, 85 | extendedFrame: 0 86 | }, 87 | src_addr: this.options.physAddr || "15.15.15", 88 | dest_addr: "0/0/0", // 89 | apdu: { 90 | // default operation is GroupValue_Write 91 | apci: 'GroupValue_Write', 92 | tpci: 0, 93 | data: 0 94 | } 95 | } 96 | } 97 | 98 | /* 99 | * submit an outbound request to the state machine 100 | * 101 | * type: service type 102 | * datagram_template: 103 | * if a datagram is passed, use this as 104 | * if a function is passed, use this to DECORATE 105 | * if NULL, then just make a new empty datagram. Look at AddXXX methods 106 | */ 107 | FSM.prototype.Request = function(type, datagram_template, callback) { 108 | // populate skeleton datagram 109 | const datagram = this.prepareDatagram(type); 110 | // decorate the datagram, if a function is passed 111 | if (typeof datagram_template == 'function') { 112 | datagram_template(datagram); 113 | } 114 | // make sure that we override the datagram service type! 115 | datagram.service_type = type; 116 | const st = KnxConstants.keyText('SERVICE_TYPE', type); 117 | // hand off the outbound request to the state machine 118 | this.handle('outbound_' + st, datagram); 119 | if (typeof callback === 'function') callback(); 120 | } 121 | 122 | // prepare a datagram for the given service type 123 | FSM.prototype.prepareDatagram = function(svcType) { 124 | const datagram = { 125 | "header_length": 6, 126 | "protocol_version": 16, // 0x10 == version 1.0 127 | "service_type": svcType, 128 | "total_length": null, // filled in automatically 129 | } 130 | // 131 | AddHPAI(datagram); 132 | // 133 | switch (svcType) { 134 | case KnxConstants.SERVICE_TYPE.CONNECT_REQUEST: 135 | AddTunn(datagram); 136 | AddCRI(datagram); // no break! 137 | case KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST: 138 | case KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST: 139 | this.AddConnState(datagram); 140 | break; 141 | case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: 142 | this.AddCEMI(datagram, KnxConstants.MESSAGECODES['L_Data.ind']); 143 | break; 144 | case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: 145 | AddTunn(datagram); 146 | this.AddTunnState(datagram); 147 | this.AddCEMI(datagram); 148 | break; 149 | case KnxConstants.SERVICE_TYPE.TUNNELING_ACK: 150 | this.AddTunnState(datagram); 151 | break; 152 | default: 153 | KnxLog.get().debug('Do not know how to deal with svc type %d', svcType); 154 | } 155 | return datagram; 156 | } 157 | 158 | /* 159 | send the datagram over the wire 160 | */ 161 | FSM.prototype.send = function(datagram, callback) { 162 | var cemitype; // TODO: set, but unused 163 | try { 164 | this.writer = KnxNetProtocol.createWriter(); 165 | switch (datagram.service_type) { 166 | case KnxConstants.SERVICE_TYPE.ROUTING_INDICATION: 167 | case KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST: 168 | // append the CEMI service type if this is a tunneling request... 169 | cemitype = KnxConstants.keyText('MESSAGECODES', datagram.cemi.msgcode); 170 | break; 171 | } 172 | const packet = this.writer.KNXNetHeader(datagram); 173 | const buf = packet.buffer; 174 | const svctype = KnxConstants.keyText('SERVICE_TYPE', datagram.service_type); // TODO: unused 175 | const descr = datagramDesc(datagram); 176 | KnxLog.get().trace('(%s): Sending %s ==> %j', this.compositeState(), descr, datagram); 177 | this.socket.send( 178 | buf, 0, buf.length, 179 | this.remoteEndpoint.port, this.remoteEndpoint.addr.toString(), 180 | (err) => { 181 | KnxLog.get().trace('(%s): UDP sent %s: %s %s', this.compositeState(), 182 | (err ? err.toString() : 'OK'), descr, buf.toString('hex') 183 | ); 184 | if (typeof callback === 'function') callback(err); 185 | } 186 | ); 187 | } catch (e) { 188 | KnxLog.get().warn(e); 189 | if (typeof callback === 'function') callback(e); 190 | } 191 | } 192 | 193 | FSM.prototype.write = function(grpaddr, value, dptid, callback) { 194 | if (grpaddr == null || value == null) { 195 | KnxLog.get().warn('You must supply both grpaddr and value!'); 196 | return; 197 | } 198 | try { 199 | // outbound request onto the state machine 200 | const serviceType = this.useTunneling ? 201 | KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : 202 | KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; 203 | this.Request(serviceType, function(datagram) { 204 | DPTLib.populateAPDU(value, datagram.cemi.apdu, dptid); 205 | datagram.cemi.dest_addr = grpaddr; 206 | }, callback); 207 | } catch (e) { 208 | KnxLog.get().warn(e); 209 | } 210 | } 211 | 212 | FSM.prototype.respond = function(grpaddr, value, dptid) { 213 | if (grpaddr == null || value == null) { 214 | KnxLog.get().warn('You must supply both grpaddr and value!'); 215 | return; 216 | } 217 | const serviceType = this.useTunneling ? 218 | KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : 219 | KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; 220 | this.Request(serviceType, function(datagram) { 221 | DPTLib.populateAPDU(value, datagram.cemi.apdu, dptid); 222 | // this is a READ request 223 | datagram.cemi.apdu.apci = "GroupValue_Response"; 224 | datagram.cemi.dest_addr = grpaddr; 225 | return datagram; 226 | }); 227 | } 228 | 229 | FSM.prototype.writeRaw = function(grpaddr, value, bitlength, callback) { 230 | if (grpaddr == null || value == null) { 231 | KnxLog.get().warn('You must supply both grpaddr and value!'); 232 | return; 233 | } 234 | if (!Buffer.isBuffer(value)) { 235 | KnxLog.get().warn('Value must be a buffer!'); 236 | return; 237 | } 238 | // outbound request onto the state machine 239 | const serviceType = this.useTunneling ? 240 | KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : 241 | KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; 242 | this.Request(serviceType, function(datagram) { 243 | datagram.cemi.apdu.data = value; 244 | datagram.cemi.apdu.bitlength = bitlength ? bitlength : (value.byteLength * 8); 245 | datagram.cemi.dest_addr = grpaddr; 246 | }, callback); 247 | } 248 | 249 | // send a READ request to the bus 250 | // you can pass a callback function which gets bound to the RESPONSE datagram event 251 | FSM.prototype.read = function(grpaddr, callback) { 252 | if (typeof callback == 'function') { 253 | // when the response arrives: 254 | const responseEvent = 'GroupValue_Response_' + grpaddr; 255 | KnxLog.get().trace('Binding connection to ' + responseEvent); 256 | const binding = (src, data) => { 257 | // unbind the event handler 258 | this.off(responseEvent, binding); 259 | // fire the callback 260 | callback(src, data); 261 | } 262 | // prepare for the response 263 | this.on(responseEvent, binding); 264 | // clean up after 3 seconds just in case no one answers the read request 265 | setTimeout( () => this.off(responseEvent, binding), 3000); 266 | } 267 | const serviceType = this.useTunneling ? 268 | KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST : 269 | KnxConstants.SERVICE_TYPE.ROUTING_INDICATION; 270 | this.Request(serviceType, function(datagram) { 271 | // this is a READ request 272 | datagram.cemi.apdu.apci = "GroupValue_Read"; 273 | datagram.cemi.dest_addr = grpaddr; 274 | return datagram; 275 | }); 276 | } 277 | 278 | FSM.prototype.Disconnect = function(cb) { 279 | var that = this; 280 | 281 | if(this.state === 'connecting') { 282 | KnxLog.get().debug('Disconnecting directly'); 283 | that.transition("uninitialized"); 284 | if(cb) { 285 | cb() 286 | } 287 | return 288 | } 289 | 290 | KnxLog.get().debug('waiting for Idle-State'); 291 | this.onIdle(function() { 292 | KnxLog.get().trace('In Idle-State'); 293 | 294 | that.on('disconnected', () => { 295 | KnxLog.get().debug('Disconnected from KNX'); 296 | if(cb) { 297 | cb() 298 | } 299 | }); 300 | 301 | KnxLog.get().debug('Disconnecting from KNX'); 302 | that.transition("disconnecting"); 303 | }); 304 | 305 | // machina.js removeAllListeners equivalent: 306 | // this.off(); 307 | } 308 | 309 | FSM.prototype.onIdle = function(cb) { 310 | if(this.state === 'idle') { 311 | KnxLog.get().trace('Connection is already Idle'); 312 | cb(); 313 | } 314 | else { 315 | this.on("transition", function(data) { 316 | if(data.toState === 'idle') { 317 | KnxLog.get().trace('Connection just transitioned to Idle'); 318 | cb(); 319 | } 320 | }); 321 | } 322 | } 323 | 324 | // return a descriptor for this datagram (TUNNELING_REQUEST_L_Data.ind) 325 | const datagramDesc = (dg) => { 326 | let blurb = KnxConstants.keyText('SERVICE_TYPE', dg.service_type); 327 | if (dg.service_type == KnxConstants.SERVICE_TYPE.TUNNELING_REQUEST || 328 | dg.service_type == KnxConstants.SERVICE_TYPE.ROUTING_INDICATION) { 329 | blurb += '_' + KnxConstants.keyText('MESSAGECODES', dg.cemi.msgcode); 330 | } 331 | return blurb; 332 | } 333 | 334 | // add the control udp local endpoint. UPDATE: not needed apparnently? 335 | const AddHPAI = (datagram) => { 336 | datagram.hpai = { 337 | protocol_type: 1, // UDP 338 | //tunnel_endpoint: this.localAddress + ":" + this.control.address().port 339 | tunnel_endpoint: '0.0.0.0:0' 340 | }; 341 | } 342 | 343 | // add the tunneling udp local endpoint UPDATE: not needed apparently? 344 | const AddTunn = (datagram) => { 345 | datagram.tunn = { 346 | protocol_type: 1, // UDP 347 | tunnel_endpoint: '0.0.0.0:0' 348 | //tunnel_endpoint: this.localAddress + ":" + this.tunnel.address().port 349 | }; 350 | } 351 | 352 | // TODO: Conncetion is obviously not a constructor, but tests call it with `new`. That should be deprecated. 353 | function Connection(options) { 354 | const conn = new FSM(options); 355 | // register with the FSM any event handlers passed into the options object 356 | if (typeof options.handlers === 'object') { 357 | for (const [key, value] of Object.entries(options.handlers)) { 358 | if (typeof value === 'function') { 359 | conn.on(key, value); 360 | } 361 | } 362 | } 363 | // boot up the KNX connection unless told otherwise 364 | if (!options.manualConnect) conn.Connect(); 365 | return conn; 366 | }; 367 | 368 | module.exports = Connection; 369 | -------------------------------------------------------------------------------- /src/FSM.js: -------------------------------------------------------------------------------- 1 | /** 2 | * knx.js - a KNX protocol stack in pure Javascript 3 | * (C) 2016-2018 Elias Karakoulakis 4 | */ 5 | 6 | const os = require('os'); 7 | const util = require('util'); 8 | const ipaddr = require('ipaddr.js'); 9 | const machina = require('machina'); 10 | const KnxConstants = require('./KnxConstants.js'); 11 | const IpRoutingConnection = require('./IpRoutingConnection.js'); 12 | const IpTunnelingConnection = require('./IpTunnelingConnection.js'); 13 | const KnxLog = require('./KnxLog.js'); 14 | 15 | module.exports = machina.Fsm.extend({ 16 | initialize(options) { 17 | this.options = options || {}; 18 | // initialise the log driver - to set the loglevel 19 | this.log = KnxLog.get(options); 20 | // set the local IP endpoint 21 | this.localAddress = null; 22 | this.ThreeLevelGroupAddressing = true; 23 | // reconnection cycle counter 24 | this.reconnection_cycles = 0; 25 | // a cache of recently sent requests 26 | this.sentTunnRequests = {}; 27 | this.useTunneling = options.forceTunneling || false; 28 | this.remoteEndpoint = { 29 | addrstring: options.ipAddr || '224.0.23.12', 30 | addr: ipaddr.parse(options.ipAddr || '224.0.23.12'), 31 | port: options.ipPort || 3671, 32 | }; 33 | const range = this.remoteEndpoint.addr.range(); 34 | this.localEchoInTunneling = 35 | typeof options.localEchoInTunneling !== 'undefined' 36 | ? options.localEchoInTunneling 37 | : false; // 14=73/2020 Supergiovane (local echo of emitEvent if in tunneling mode) 38 | this.log.debug( 39 | 'initializing %s connection to %s', 40 | range, 41 | this.remoteEndpoint.addrstring 42 | ); 43 | switch (range) { 44 | case 'multicast': 45 | if (this.localEchoInTunneling) { 46 | this.localEchoInTunneling = false; 47 | this.log.debug( 48 | 'localEchoInTunneling: true but DISABLED because i am on multicast' 49 | ); 50 | } // 14/03/2020 Supergiovane: if multicast, disable the localEchoInTunneling, because there is already an echo 51 | IpRoutingConnection(this); 52 | break; 53 | case 'unicast': 54 | case 'private': 55 | case 'loopback': 56 | this.useTunneling = true; 57 | IpTunnelingConnection(this); 58 | break; 59 | default: 60 | throw util.format( 61 | 'IP address % (%s) cannot be used for KNX', 62 | options.ipAddr, 63 | range 64 | ); 65 | } 66 | }, 67 | 68 | namespace: 'knxnet', 69 | 70 | initialState: 'uninitialized', 71 | 72 | states: { 73 | uninitialized: { 74 | ['*']() { 75 | this.transition('connecting'); 76 | }, 77 | }, 78 | 79 | jumptoconnecting: { 80 | _onEnter() { 81 | this.transition('connecting'); 82 | }, 83 | }, 84 | 85 | connecting: { 86 | _onEnter() { 87 | // tell listeners that we disconnected 88 | // putting this here will result in a correct state for our listeners 89 | this.emit('disconnected'); 90 | this.log.debug(util.format('useTunneling=%j', this.useTunneling)); 91 | if (this.useTunneling) { 92 | let connection_attempts = 0; 93 | if (!this.localAddress) 94 | throw 'Not bound to an IPv4 non-loopback interface'; 95 | this.log.debug( 96 | util.format('Connecting via %s...', this.localAddress) 97 | ); 98 | // we retry 3 times, then restart the whole cycle using a slower and slower rate (max delay is 5 minutes) 99 | this.connecttimer = setInterval(() => { 100 | connection_attempts += 1; 101 | if (connection_attempts >= 3) { 102 | clearInterval(this.connecttimer); 103 | // quite a few KNXnet/IP devices drop any tunneling packets received via multicast 104 | if (this.remoteEndpoint.addr.range() == 'multicast') { 105 | this.log.warn( 106 | 'connection timed out, falling back to pure routing mode...' 107 | ); 108 | this.usingMulticastTunneling = true; 109 | this.transition('connected'); 110 | } else { 111 | // we restart the connection cycle with a growing delay (max 5 minutes) 112 | this.reconnection_cycles += 1; 113 | const delay = Math.min(this.reconnection_cycles * 3, 300); 114 | this.log.debug( 115 | 'reattempting connection in ' + delay + ' seconds' 116 | ); 117 | setTimeout( 118 | // restart connecting cycle (cannot jump straight to 'connecting' so we use an intermediate state) 119 | () => this.transition('jumptoconnecting'), 120 | delay * 1000 121 | ); 122 | } 123 | } else { 124 | this.log.warn('connection timed out, retrying...'); 125 | this.send( 126 | this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) 127 | ); 128 | } 129 | }, 3000); 130 | delete this.channel_id; 131 | delete this.conntime; 132 | delete this.lastSentTime; 133 | // send connect request directly 134 | this.send( 135 | this.prepareDatagram(KnxConstants.SERVICE_TYPE.CONNECT_REQUEST) 136 | ); 137 | } else { 138 | // no connection sequence needed in pure multicast routing 139 | this.transition('connected'); 140 | } 141 | }, 142 | _onExit() { 143 | clearInterval(this.connecttimer); 144 | }, 145 | inbound_CONNECT_RESPONSE(datagram) { 146 | this.log.debug(util.format('got connect response')); 147 | if ( 148 | datagram.hasOwnProperty('connstate') && 149 | datagram.connstate.status === 150 | KnxConstants.RESPONSECODE.E_NO_MORE_CONNECTIONS 151 | ) { 152 | try { 153 | this.socket.close(); 154 | } catch (error) {} 155 | this.transition('uninitialized'); 156 | this.emit('disconnected'); 157 | this.log.debug( 158 | 'The KNXnet/IP server rejected the data connection (Maximum connections reached). Waiting 1 minute before retrying...' 159 | ); 160 | setTimeout(() => { 161 | this.Connect(); 162 | }, 60000); 163 | } else { 164 | // store channel ID into the Connection object 165 | this.channel_id = datagram.connstate.channel_id; 166 | // send connectionstate request directly 167 | this.send( 168 | this.prepareDatagram( 169 | KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST 170 | ) 171 | ); 172 | // TODO: handle send err 173 | } 174 | }, 175 | inbound_CONNECTIONSTATE_RESPONSE(datagram) { 176 | if (this.useTunneling) { 177 | const str = KnxConstants.keyText( 178 | 'RESPONSECODE', 179 | datagram.connstate.status 180 | ); 181 | this.log.debug( 182 | util.format( 183 | 'Got connection state response, connstate: %s, channel ID: %d', 184 | str, 185 | datagram.connstate.channel_id 186 | ) 187 | ); 188 | this.transition('connected'); 189 | } 190 | }, 191 | ['*'](data) { 192 | this.log.debug(util.format('*** deferring Until Transition %j', data)); 193 | this.deferUntilTransition('idle'); 194 | }, 195 | }, 196 | 197 | connected: { 198 | _onEnter() { 199 | // Reset connection reattempts cycle counter for next disconnect 200 | this.reconnection_cycles = 0; 201 | // Reset outgoing sequence counter.. 202 | this.seqnum = -1; 203 | /* important note: the sequence counter is SEPARATE for incoming and 204 | outgoing datagrams. We only keep track of the OUTGOING L_Data.req 205 | and we simply acknowledge the incoming datagrams with their own seqnum */ 206 | this.lastSentTime = this.conntime = Date.now(); 207 | this.log.debug( 208 | util.format( 209 | '--- Connected in %s mode ---', 210 | this.useTunneling ? 'TUNNELING' : 'ROUTING' 211 | ) 212 | ); 213 | this.transition('idle'); 214 | this.emit('connected'); 215 | }, 216 | }, 217 | 218 | disconnecting: { 219 | // TODO: skip on pure routing 220 | _onEnter() { 221 | if (this.useTunneling) { 222 | const aliveFor = this.conntime ? Date.now() - this.conntime : 0; 223 | KnxLog.get().debug( 224 | '(%s):\tconnection alive for %d seconds', 225 | this.compositeState(), 226 | aliveFor / 1000 227 | ); 228 | this.disconnecttimer = setTimeout(() => { 229 | KnxLog.get().debug( 230 | '(%s):\tconnection timed out', 231 | this.compositeState() 232 | ); 233 | try { 234 | this.socket.close(); 235 | } catch (error) {} 236 | this.transition('uninitialized'); 237 | this.emit('disconnected'); 238 | }, 3000); 239 | // 240 | this.send( 241 | this.prepareDatagram(KnxConstants.SERVICE_TYPE.DISCONNECT_REQUEST), 242 | (err) => { 243 | // TODO: handle send err 244 | KnxLog.get().debug( 245 | '(%s):\tsent DISCONNECT_REQUEST', 246 | this.compositeState() 247 | ); 248 | } 249 | ); 250 | } 251 | }, 252 | _onExit() { 253 | clearTimeout(this.disconnecttimer); 254 | }, 255 | inbound_DISCONNECT_RESPONSE(datagram) { 256 | if (this.useTunneling) { 257 | KnxLog.get().debug( 258 | '(%s):\tgot disconnect response', 259 | this.compositeState() 260 | ); 261 | try { 262 | this.socket.close(); 263 | } catch (error) {} 264 | this.transition('uninitialized'); 265 | this.emit('disconnected'); 266 | } 267 | }, 268 | }, 269 | 270 | idle: { 271 | _onEnter() { 272 | if (this.useTunneling) { 273 | if (this.idletimer == null) { // set one 274 | // time out on inactivity... 275 | this.idletimer = setTimeout( () => { 276 | this.transition('requestingConnState'); 277 | clearTimeout(this.idletimer); 278 | this.idletimer = null; 279 | }, 60000); 280 | } 281 | } 282 | // debuglog the current FSM state plus a custom message 283 | KnxLog.get().debug('(%s):\t%s', this.compositeState(), ' zzzz...'); 284 | // process any deferred items from the FSM internal queue 285 | this.processQueue(); 286 | }, 287 | _onExit() { 288 | //clearTimeout(this.idletimer); 289 | }, 290 | // while idle we can either... 291 | 292 | // 1) queue an OUTGOING routing indication... 293 | outbound_ROUTING_INDICATION(datagram) { 294 | const elapsed = Date.now() - this.lastSentTime; 295 | // if no miminum delay set OR the last sent datagram was long ago... 296 | if ( 297 | !this.options.minimumDelay || 298 | elapsed >= this.options.minimumDelay 299 | ) { 300 | // ... send now 301 | this.transition('sendDatagram', datagram); 302 | } else { 303 | // .. or else, let the FSM handle it later 304 | setTimeout( 305 | () => this.handle('outbound_ROUTING_INDICATION', datagram), 306 | this.minimumDelay - elapsed 307 | ); 308 | } 309 | }, 310 | 311 | // 2) queue an OUTGOING tunelling request... 312 | outbound_TUNNELING_REQUEST(datagram) { 313 | if (this.useTunneling) { 314 | const elapsed = Date.now() - this.lastSentTime; 315 | // if no miminum delay set OR the last sent datagram was long ago... 316 | if ( 317 | !this.options.minimumDelay || 318 | elapsed >= this.options.minimumDelay 319 | ) { 320 | // ... send now 321 | this.transition('sendDatagram', datagram); 322 | } else { 323 | // .. or else, let the FSM handle it later 324 | setTimeout( 325 | () => this.handle('outbound_TUNNELING_REQUEST', datagram), 326 | this.minimumDelay - elapsed 327 | ); 328 | } 329 | } else { 330 | KnxLog.get().debug( 331 | "(%s):\tdropping outbound TUNNELING_REQUEST, we're in routing mode", 332 | this.compositeState() 333 | ); 334 | } 335 | }, 336 | 337 | // 3) receive an INBOUND tunneling request INDICATION (L_Data.ind) 338 | ['inbound_TUNNELING_REQUEST_L_Data.ind'](datagram) { 339 | if (this.useTunneling) { 340 | this.transition('recvTunnReqIndication', datagram); 341 | } 342 | }, 343 | 344 | /* 4) receive an INBOUND tunneling request CONFIRMATION (L_Data.con) to one of our sent tunnreq's 345 | * We don't need to explicitly wait for a L_Data.con confirmation that the datagram has in fact 346 | * reached its intended destination. This usually requires setting the 'Sending' flag 347 | * in ETS, usually on the 'primary' device that contains the actuator endpoint 348 | */ 349 | ['inbound_TUNNELING_REQUEST_L_Data.con'](datagram) { 350 | if (this.useTunneling) { 351 | const confirmed = this.sentTunnRequests[datagram.cemi.dest_addr]; 352 | if (confirmed) { 353 | delete this.sentTunnRequests[datagram.cemi.dest_addr]; 354 | this.emit('confirmed', confirmed); 355 | } 356 | KnxLog.get().trace( 357 | '(%s): %s %s', 358 | this.compositeState(), 359 | datagram.cemi.dest_addr, 360 | confirmed 361 | ? 'delivery confirmation (L_Data.con) received' 362 | : 'unknown dest addr' 363 | ); 364 | this.acknowledge(datagram); 365 | } 366 | }, 367 | 368 | // 5) receive an INBOUND ROUTING_INDICATION (L_Data.ind) 369 | ['inbound_ROUTING_INDICATION_L_Data.ind'](datagram) { 370 | this.emitEvent(datagram); 371 | }, 372 | 373 | inbound_DISCONNECT_REQUEST(datagram) { 374 | if (this.useTunneling) { 375 | this.transition('connecting'); 376 | } 377 | }, 378 | }, 379 | 380 | // if idle for too long, request connection state from the KNX IP router 381 | requestingConnState: { 382 | _onEnter() { 383 | // added to note sending connectionstate_request 384 | KnxLog.get().debug( 'Requesting Connection State'); 385 | KnxLog.get().trace( 386 | '(%s): Requesting Connection State', 387 | this.compositeState() 388 | ); 389 | this.send( 390 | this.prepareDatagram( 391 | KnxConstants.SERVICE_TYPE.CONNECTIONSTATE_REQUEST 392 | ) 393 | ); 394 | // TODO: handle send err 395 | // 396 | this.connstatetimer = setTimeout(() => { 397 | const msg = 'timed out waiting for CONNECTIONSTATE_RESPONSE'; 398 | KnxLog.get().trace('(%s): %s', this.compositeState(), msg); 399 | this.transition('connecting'); 400 | this.emit('error', msg); 401 | }, 1000); 402 | }, 403 | _onExit() { 404 | clearTimeout(this.connstatetimer); 405 | }, 406 | inbound_CONNECTIONSTATE_RESPONSE(datagram) { 407 | const state = KnxConstants.keyText( 408 | 'RESPONSECODE', 409 | datagram.connstate.status 410 | ); 411 | switch (datagram.connstate.status) { 412 | case 0: 413 | this.transition('idle'); 414 | break; 415 | default: 416 | this.log.debug( 417 | util.format( 418 | '*** error: %s *** (connstate.code: %d)', 419 | state, 420 | datagram.connstate.status 421 | ) 422 | ); 423 | this.transition('connecting'); 424 | this.emit('error', state); 425 | } 426 | }, 427 | ['*'](data) { 428 | this.log.debug( 429 | util.format( 430 | '*** deferring %s until transition from requestingConnState => idle', 431 | data.inputType 432 | ) 433 | ); 434 | this.deferUntilTransition('idle'); 435 | }, 436 | }, 437 | 438 | /* 439 | * 1) OUTBOUND DATAGRAM (ROUTING_INDICATION or TUNNELING_REQUEST) 440 | */ 441 | sendDatagram: { 442 | _onEnter(datagram) { 443 | // send the telegram on the wire 444 | this.seqnum += 1; 445 | if (this.useTunneling) datagram.tunnstate.seqnum = this.seqnum & 0xff; 446 | this.send(datagram, (err) => { 447 | if (err) { 448 | //console.trace('error sending datagram, going idle'); 449 | this.seqnum -= 1; 450 | this.transition('idle'); 451 | } else { 452 | // successfully sent the datagram 453 | if (this.useTunneling) 454 | this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; 455 | this.lastSentTime = Date.now(); 456 | this.log.debug( 457 | '(%s):\t>>>>>>> successfully sent seqnum: %d', 458 | this.compositeState(), 459 | this.seqnum 460 | ); 461 | if (this.useTunneling) { 462 | // and then wait for the acknowledgement 463 | this.transition('sendTunnReq_waitACK', datagram); 464 | } else { 465 | this.transition('idle'); 466 | } 467 | } 468 | // 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. 469 | // ######################## 470 | //if (this.useTunneling) this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; 471 | if (this.useTunneling) { 472 | this.sentTunnRequests[datagram.cemi.dest_addr] = datagram; 473 | if ( 474 | typeof this.localEchoInTunneling !== 'undefined' && 475 | this.localEchoInTunneling 476 | ) { 477 | try { 478 | this.emitEvent(datagram); 479 | this.log.debug( 480 | '(%s):\t>>>>>>> localEchoInTunneling: echoing by emitting %d', 481 | this.compositeState(), 482 | this.seqnum 483 | ); 484 | } catch (error) { 485 | this.log.debug( 486 | '(%s):\t>>>>>>> localEchoInTunneling: error echoing by emitting %d ' + 487 | error, 488 | this.compositeState(), 489 | this.seqnum 490 | ); 491 | } 492 | } 493 | } 494 | // ######################## 495 | }); 496 | }, 497 | ['*'](data) { 498 | this.log.debug( 499 | util.format( 500 | '*** deferring %s until transition sendDatagram => idle', 501 | data.inputType 502 | ) 503 | ); 504 | this.deferUntilTransition('idle'); 505 | }, 506 | }, 507 | /* 508 | * Wait for tunneling acknowledgement by the IP router; this means the sent UDP packet 509 | * reached the IP router and NOT that the datagram reached its final destination 510 | */ 511 | sendTunnReq_waitACK: { 512 | _onEnter(datagram) { 513 | //this.log.debug('setting up tunnreq timeout for %j', datagram); 514 | this.tunnelingAckTimer = setTimeout(() => { 515 | this.log.debug('timed out waiting for TUNNELING_ACK'); 516 | // TODO: resend datagram, up to 3 times 517 | this.transition('idle'); 518 | this.emit('tunnelreqfailed', datagram); 519 | }, 2000); 520 | }, 521 | _onExit() { 522 | clearTimeout(this.tunnelingAckTimer); 523 | }, 524 | inbound_TUNNELING_ACK(datagram) { 525 | this.log.debug( 526 | util.format( 527 | '===== datagram %d acknowledged by IP router', 528 | datagram.tunnstate.seqnum 529 | ) 530 | ); 531 | this.transition('idle'); 532 | }, 533 | ['*'](data) { 534 | this.log.debug( 535 | util.format( 536 | '*** deferring %s until transition sendTunnReq_waitACK => idle', 537 | data.inputType 538 | ) 539 | ); 540 | this.deferUntilTransition('idle'); 541 | }, 542 | }, 543 | 544 | /* 545 | * 2) INBOUND tunneling request (L_Data.ind) - only in tunnelling mode 546 | */ 547 | recvTunnReqIndication: { 548 | _onEnter(datagram) { 549 | this.seqnumRecv = datagram.tunnstate.seqnum; 550 | this.acknowledge(datagram); 551 | this.transition('idle'); 552 | this.emitEvent(datagram); 553 | }, 554 | ['*'](data) { 555 | this.log.debug(util.format('*** deferring Until Transition %j', data)); 556 | this.deferUntilTransition('idle'); 557 | }, 558 | }, 559 | }, 560 | 561 | acknowledge(datagram) { 562 | const ack = this.prepareDatagram( 563 | KnxConstants.SERVICE_TYPE.TUNNELING_ACK, 564 | datagram 565 | ); 566 | /* acknowledge by copying the inbound datagram's sequence counter */ 567 | ack.tunnstate.seqnum = datagram.tunnstate.seqnum; 568 | this.send(ack, (err) => { 569 | // TODO: handle send err 570 | }); 571 | }, 572 | 573 | emitEvent(datagram) { 574 | // emit events to our beloved subscribers in a multitude of targets 575 | // ORDER IS IMPORTANT! 576 | const evtName = datagram.cemi.apdu.apci; 577 | // 1. 578 | // 'event_', ''GroupValue_Write', src, data 579 | this.emit( 580 | util.format('event_%s', datagram.cemi.dest_addr), 581 | evtName, 582 | datagram.cemi.src_addr, 583 | datagram.cemi.apdu.data 584 | ); 585 | // 2. 586 | // 'GroupValue_Write_1/2/3', src, data 587 | this.emit( 588 | util.format('%s_%s', evtName, datagram.cemi.dest_addr), 589 | datagram.cemi.src_addr, 590 | datagram.cemi.apdu.data 591 | ); 592 | // 3. 593 | // 'GroupValue_Write', src, dest, data 594 | this.emit( 595 | evtName, 596 | datagram.cemi.src_addr, 597 | datagram.cemi.dest_addr, 598 | datagram.cemi.apdu.data 599 | ); 600 | // 4. 601 | // 'event', 'GroupValue_Write', src, dest, data 602 | this.emit( 603 | 'event', 604 | evtName, 605 | datagram.cemi.src_addr, 606 | datagram.cemi.dest_addr, 607 | datagram.cemi.apdu.data 608 | ); 609 | }, 610 | 611 | getLocalAddress() { 612 | const candidateInterfaces = this.getIPv4Interfaces(); 613 | // if user has declared a desired interface then use it 614 | if (this.options && this.options.interface) { 615 | const iface = candidateInterfaces[this.options.interface]; 616 | if (!iface) 617 | throw new Error( 618 | 'Interface ' + 619 | this.options.interface + 620 | ' not found or has no useful IPv4 address!' 621 | ); 622 | 623 | return candidateInterfaces[this.options.interface].address; 624 | } 625 | // just return the first available IPv4 non-loopback interface 626 | const first = Object.values(candidateInterfaces)[0]; 627 | if (first) return first.address; 628 | 629 | // no local IpV4 interfaces? 630 | throw 'No valid IPv4 interfaces detected'; 631 | }, 632 | 633 | // get the local address of the IPv4 interface we're going to use 634 | getIPv4Interfaces() { 635 | const candidateInterfaces = {}; 636 | const interfaces = os.networkInterfaces(); 637 | for (const [iface, addrs] of Object.entries(interfaces)) { 638 | for (const addr of addrs) { 639 | if ([4, 'IPv4'].indexOf(addr.family) > -1 && !addr.internal) { 640 | this.log.trace( 641 | util.format('candidate interface: %s (%j)', iface, addr) 642 | ); 643 | candidateInterfaces[iface] = addr; 644 | } 645 | } 646 | } 647 | return candidateInterfaces; 648 | }, 649 | }); 650 | --------------------------------------------------------------------------------