UNPKG

knxultimate

Version:

KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.

354 lines (329 loc) 8.42 kB
import { module } from '../KnxLog' import type { DatapointConfig } from '.' const logger = module('DPT60001') function toRadix(value: number, radix: number) { if (!Number.isSafeInteger(value)) { logger.error('value must be a safe integer') } const digits = Math.ceil(64 / Math.log2(radix)) const twosComplement = value < 0 ? BigInt(radix) ** BigInt(digits) + BigInt(value) : value return twosComplement.toString(radix).padStart(digits, '0') } function griesserSectorCode(Byte0: number, Byte1: number) { const SectorCode = Byte0 + (Byte1 & 3) * 256 return SectorCode } function griesserCommandCode(Byte1: number) { const command = (Byte1 / 4) >> 0 return command } function griesserParameter( command: number, Byte2: number, Byte3: number, Byte4: number, Byte5: number, ) { let commandOperation: string switch (command) { case 1: // drive command switch (Byte2 & 31) { case 0: return 'no driving movement' case 1: return 'upper end position' case 2: return 'lower end position' case 3: if (Byte3 >= 1 && Byte3 <= 4) { return `fixed position P${Byte3} approach` } return `Unknown value for Pn ${Byte3}` default: return `Unknown drive command${Byte2}` } case 4: // set/delete lock if (Byte2 === 0) { return 'no lock' } switch (Byte2 & 3) { case 1: return 'driving command' case 2: return 'button lock' case 3: return 'driving command- and button lock' } if (Byte3 === 0) { return 'delete lock' } return 'set lock' case 5: // operation code if (Byte2 <= 6) { commandOperation = 'groupoperation' } else if (Byte2 >= 128 && Byte2 <= 134) { commandOperation = 'localoperation' } else { commandOperation = `unknown command Byte3 for ${Byte2}` } if ((Byte2 & 127) === 0) { return [commandOperation, 'long up'] } if ((Byte2 & 127) === 1) { return [commandOperation, 'long down'] } if ((Byte2 & 127) === 2) { return [commandOperation, 'short up'] } if ((Byte2 & 127) === 3) { return [commandOperation, 'short down'] } if ((Byte2 & 127) === 4) { return [commandOperation, 'stop'] } if ((Byte2 & 127) === 5) { return [commandOperation, 'long-short up'] } if ((Byte2 & 127) === 6) { return [commandOperation, 'long-short down'] } return [commandOperation, `unknown command Byte3 for ${Byte2}`] case 22: // driving range limits for automatic button commands return [ `min. angle: ${Byte2}`, `max. angle: ${Byte3}`, `min. height: ${Byte4}`, `max. height: ${Byte5}`, ] default: return `unknown value for command: ${command}` } } function griesserSectors(SectorCode: number) { let SectorMin: number let SectorMax: number let dA: number let a: number let SectorCodeMin: number let SectorCodeMax: number dA = 1 a = SectorCode if (a > 0) { while ((a & 1) === 0) { a = (a / 2) >> 0 dA *= 2 } dA -= 1 SectorMin = SectorCode - dA SectorMax = SectorCode + dA } else { SectorMin = 0 SectorMax = 0 } if (SectorMin === 0) { SectorCodeMin = 0 } else { SectorCodeMin = (((SectorMin - 1) / 2) >> 0) + 1 } if (SectorMax === 0) { SectorCodeMax = 0 } else { SectorCodeMax = (((SectorMax - 1) / 2) >> 0) + 1 } if (SectorCodeMax === SectorCodeMin) { return [SectorCodeMin] } const Sectors = [] for (let i = SectorCodeMin; i <= SectorCodeMax; i++) { Sectors.push(i) } return Sectors } function griesserSectorToSectorCode(sectors: number[]) { if (sectors.length === 1) { return sectors[0] + sectors[0] - 1 } return Math.min(...sectors) + Math.max(...sectors) - 1 } function griesserCommandToCommandCode(command: string) { switch (command) { case 'operation code': return 5 default: logger.error(`not implemented yet: ${command}`) return null } } function griesserCommandToCommandCodeP1(command: string) { switch (command) { case 'long up': return 128 case 'long down': return 129 case 'short up': return 130 case 'short down': return 131 case 'stop': return 132 case 'long-short up': return 133 case 'long-short down': return 134 default: logger.error(`unknown command: ${command}`) return null } } function griesserCommand(command: number) { switch (command) { case 1: return 'drive command' case 2: return 'value correction' case 3: return 'automatic state' case 4: return 'set/delete lock' case 5: return 'operation code' case 6: return 'set scene' case 7: return 'special command' case 8: return 'date' case 9: return 'sync time' case 10: return 'sensor reading notification' case 11: return 'bus monitoring' case 16: return 'driving range limits for safety drive commands' case 17: return 'driving range limits for safety drive commands' case 19: return 'driving range limits for safety drive commands' case 20: return 'driving range limits for safety drive commands' case 22: return 'driving range limits for automatic drive commands' case 23: return 'driving range limits for automatic drive commands' case 24: return 'driving range limits for automatic drive commands' default: return `unknown value for function: ${command}` } } function griesserPrio(prio: number, command: number) { const prioCommand = ((command & 224) / 32) >> 0 if (((prio & 252) / 4) >> 0 === 0) { switch (prioCommand) { case 0: return 'border command' case 1: return 'automatic command' case 3: return 'priority command' case 4: return 'warning command' case 5: return 'security command' case 6: return 'danger command' default: return `unknown priority${prioCommand}` } } else { return '-' } } // Send to BUS const config: DatapointConfig = { id: 'DPT60001', formatAPDU(value) { if (!value) { logger.error('cannot write null value') return null } if ( typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'command') && Object.prototype.hasOwnProperty.call(value, 'data') && Object.prototype.hasOwnProperty.call(value, 'sectors') && value.data[0] === 'localoperation' ) { const sectorCode = griesserSectorToSectorCode(value.sectors) const commandCode = griesserCommandToCommandCode(value.command) const p1 = griesserCommandToCommandCodeP1(value.data[1]) const bufferTotal = Buffer.alloc(6) bufferTotal[0] = parseInt(toRadix(sectorCode, 2).slice(-8), 2) bufferTotal[1] = parseInt( toRadix(commandCode, 2).slice(-6) + toRadix(sectorCode, 2).slice(-10, -8), 2, ) bufferTotal[2] = parseInt(toRadix(p1, 2).slice(-8), 2) return bufferTotal } logger.error( 'Must supply an value {command:"operation code", data:["localoperation", "long up"], sectors:[159]}', ) return null }, // RX from BUS fromBuffer(buf) { if (buf.length !== 6) { logger.warn( 'DPTGriesser.fromBuffer: buf should be 6 bytes long (got %d bytes)', buf.length, ) return null } const hexToDecimal = (hex) => parseInt(hex, 16) const bufTotale = buf.toString('hex') const Byte0 = hexToDecimal(bufTotale.slice(0, 2)) const Byte1 = hexToDecimal(bufTotale.slice(2, 4)) const Byte2 = hexToDecimal(bufTotale.slice(4, 6)) const Byte3 = hexToDecimal(bufTotale.slice(6, 8)) const Byte4 = hexToDecimal(bufTotale.slice(8, 10)) const Byte5 = hexToDecimal(bufTotale.slice(10, 12)) const sectorCode = griesserSectorCode(Byte0, Byte1) const commandCode = griesserCommandCode(Byte1) return { Byte0, Byte1, Byte2, Byte3, Byte4, Byte5, sectorCode, commandCode, sectors: griesserSectors(sectorCode), prio: griesserPrio(Byte1, Byte2), command: griesserCommand(commandCode), data: griesserParameter(commandCode, Byte2, Byte3, Byte4, Byte5), } }, // DPT Griesser Object basetype info basetype: { bitlength: 4 * 8 + 2 * 6 + 1 * 10, valuetype: 'composite', desc: 'Commands for solar shading actors', help: `// Sample of 60001. // Now are only the local operation implemented. // For example, for 60001, set the sector 42 localy up. msg.payload = { command: "operation code", data: ["localoperation", "long up"], sectors: [42] }; return msg;`, }, subtypes: { '001': { desc: 'DPT_Griesser_Object', name: 'Griesser Object', }, }, } export default config