UNPKG

hb-velux-tools

Version:
1,450 lines (1,411 loc) 52.9 kB
// hb-velux-tools/lib/VeluxClient.js // Copyright © 2025-2026 Erik Baauw. All rights reserved. // // Homebridge Velux Tools. // import { isIPv4 } from 'node:net' import { toHexString } from 'hb-lib-tools' import { OptionParser } from 'hb-lib-tools/OptionParser' function checkData (data, length) { if (!Buffer.isBuffer(data)) { throw new Error('invalid data') } if (data.length !== length) { throw new Error(`${data.length}: invalid data length (expected ${length})`) } } function decodeActuatorType (actuatorType) { return { 0x0040: 'Interior Venetian Blind', 0x0080: 'Roller Shutter', 0x0081: 'Roller Shutter with Adjustable Slats', 0x0082: 'Roller Shutter with Projection', 0x00C0: 'Vertical Exterior Awning', 0x0100: 'Window Opener', 0x0101: 'Window Opener with Rain Sensor', 0x0140: 'Garage Door Opener', 0x017A: 'Garage Door Opener', 0x0180: 'Light', 0x01BA: 'On/Off Light', 0x01C0: 'Gate Opener', 0x01FA: 'Gate Opener', 0x0240: 'Door Lock', 0x0241: 'Window Lock', 0x0280: 'Vertical Interior Blind', 0x0340: 'Dual Roller Shutter', 0x03C0: 'On/Off Switch', 0x0400: 'Horizontal Awning', 0x0440: 'Exterior Venetian Blind', 0x0480: 'Louver Blind', 0x04C0: 'Curtain Track', 0x0500: 'Ventilation Point', 0x0501: 'Air Inlet', 0x0502: 'Air Transfer', 0x0503: 'Air Outlet', 0x0540: 'Exterior Heating', 0x057A: 'Exterior Heating', 0x0600: 'Swinging Shutter', 0x0601: 'Swinging Shutter' }[actuatorType] ?? '0x' + toHexString(actuatorType, 4) } function decodeGroupInformation (data) { checkData(data, 99) const groupType = data.readUint8(70) const nNodes = data.readUInt8(71) const nodeIds = [] if (groupType === 0 && nNodes > 0) { // User. let mask for (let i = 0; i < 200; i++) { if (i % 8 === 0) { mask = data.readUint8(72 + i / 8) } if (mask & 0x01) { nodeIds.push(i) } mask >>= 1 } } return { groupId: data.readUInt8(0), order: data.readUInt16BE(1), placement: data.readUInt8(3), name: data.subarray(4, data.indexOf(0, 4)).toString(), velocity: decodeVelocity(data.readUInt8(68)), nodeVariation: data.readUInt16BE(69), groupType, nNodes, nodeIds, revision: data.readUInt16BE(97) } } function decodeIpv4 (ipv4) { return [ ((ipv4 & 0xFF000000) >> 24) & 0xFF, (ipv4 & 0x00FF0000) >> 16, (ipv4 & 0x0000FF00) >> 8, ipv4 & 0x000000FF ].join('.') } function encodeIpv4 (ipv4) { const a = ipv4.split('.') return a[0] << 24 | a[1] << 16 | a[2] << 8 | a[3] } function decodeManufacturerId (manufacturerId) { return { 1: 'VELUX', 2: 'Somfy', 3: 'Honeywell', 4: 'Hörmann', 5: 'ASSA ABLOY', 6: 'Niko', 7: 'WINDOW MASTER', 8: 'Renson', 9: 'CIAT', 10: 'Secuyou', 11: 'OVERKIZ', 12: 'Atlantic Group' }[manufacturerId] ?? '0x' + toHexString(manufacturerId, 2) } function decodeNodeInformation (data) { checkData(data, 124) return { nodeId: data.readUInt8(0), order: data.readUInt16BE(1), placement: data.readUInt8(3), name: data.subarray(4, data.indexOf(0, 4)).toString(), velocity: decodeVelocity(data.readUInt8(68)), nodeType: data.readUInt16BE(69), // nodeType: decodeType(data.readUInt16BE(69)), productGroup: data.readUInt8(71), productType: data.readUint8(72), nodeVariation: data.readUint8(73), powerMode: data.readUint8(74), buildNumber: data.readUint8(75), serialNumber: data.toString('hex', 76, 83).toUpperCase(), state: data.readUint8(84), currentPosition: decodePosition(data.readUint16BE(85)), targetPosition: decodePosition(data.readUint16BE(87)), fp1Position: decodePosition(data.readUint16BE(89)), fp2Position: decodePosition(data.readUint16BE(91)), fp3Position: decodePosition(data.readUint16BE(93)), fp4Position: decodePosition(data.readUint16BE(95)), remainingTime: data.readUint16BE(97), timeStamp: new Date(data.readUint32BE(99) * 1000).toISOString(), nAlias: data.readUInt8(103) } } function decodeNodeParameter (parameter) { if (parameter === 0x00) { return 'MP' } if (parameter <= 0x10) { return 'FP' + parameter`` } if (parameter === 0xFF) { return 'not used' } return toHexString(parameter, 2) } function decodePosition (position) { switch (position) { case 0xD100: return 'target' case 0xD200: return 'current' case 0xD300: return 'default' case 0xD400: return 'ignore' case 0xF7FF: case 0xFFFF: return 'unknown' default: if (position <= 0xC800) { return Math.round(position / 0x0200) } if (position >= 0xC900 && position <= 0xD0D0) { return { delta: (Math.round((position - 0xC900) / 10) - 100) } } throw new Error(`${toHexString(position, 4)}: invalid position`) } } function encodePosition (position) { switch (position) { case 'target': return 0xD100 case 'current': return 0xD200 case 'default': return 0xD300 case 'ignore': return 0xD400 default: if (position >= 0 && position <= 100) { return position * 0x0200 } if (position.delta >= -100 && position.delta <= 100) { return (position.delta + 100) * 10 + 0xC900 } throw new Error(`${position}: invalid position`) } } function decodePowerState (powerState) { return { // _raw: '0x' + toHexString(powerState, 2), powerSaveMode: powerState & 0x03, ioMembership: (powerState & 0x04) >> 2, rfSupport: (powerState & 0x08) >> 3, turnaroundTime: (powerState & 0xC0) >> 6 } } function decodeRunStatus (runStatus) { return { 0x00: 'completed', 0x01: 'failed', 0x02: 'active' }[runStatus] ?? '0x' + toHexString(runStatus, 2) } function decodeSessionStatus (data) { checkData(data, 3) const sessionId = data.readUInt16BE(0) const status = data.readUInt8(2) if (status === 0) { throw new Error('request failed') } return { sessionId } } function decodeStatus (data, message = 'request failed') { checkData(data, 1) const status = data.readUInt8() if (status !== 0) { throw new Error(message) } } function decodeStatusId (data, id = 'nodeId') { checkData(data, 2) const status = data.readUInt8() if (status === 0) { const response = {} response[id] = data.readUInt8(1) return response } const message = { 1: 'request failed', 2: 'invalid ' + id }[status] ?? 'status ' + status throw new Error(message) } function decodeStatusInput (data) { checkData(data, 2) const status = data.readUInt8(1) if (status === 1) { return { inputId: data.readUInt8(0) } } const message = { 0: 'request failed' }[status] ?? 'status ' + status throw new Error(message) } // function decodeType (fullType) { // return { // // _raw: toHexString(type, 4), // type: (fullType & 0xFFC0) >> 6, // subtype: fullType & 0x3F // } // } function decodeVelocity (velocity) { return { 0: 'default', // = fast 1: 'slow', 2: 'fast', 255: 'not supported' }[velocity] ?? '0x' + toHexString(velocity, 2) } function encodeVelocity (velocity) { return { default: 0, // = fast slow: 1, fast: 2 }[velocity] ?? 0 } /** Gateway API commands. */ const commands = Object.freeze({ // ===== 5. Authentication ================================================== /** 5.1.1 - Enter password to authenticate request * @member {function} * @params {String} password * @returns true */ GW_PASSWORD_ENTER_REQ: { // 5.1.1 - Enter password to authenticate request id: 0x3000, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toString('params.password', params.password, true, userInput) const data = Buffer.allocUnsafe(32).fill(0) data.write(params.password) params.password = params.password.replace(/./g, '*') return data } }, GW_PASSWORD_ENTER_CFM: { // 5.1.2 - Acknowledge to GW_PASSWORD_ENTER_REQ id: 0x3001, req: 0x3000, // GW_PASSWORD_ENTER_REQ decode: (data) => { return decodeStatus(data, 'invalid password') } }, GW_PASSWORD_CHANGE_REQ: { // 5.1.3 - Request password change. id: 0x3002, ntf: true, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toString('params.oldPassword', params.oldPassword, true, userInput) OptionParser.toString('params.newPassword', params.newPassword, true, userInput) const data = Buffer.allocUnsafe(64).fill(0) data.write(params.oldPassword) params.oldPassword = params.oldPassword.replace(/./g, '*') data.write(params.newPassword, 32) params.newPassword = params.newPassword.replace(/./g, '*') return data } }, GW_PASSWORD_CHANGE_CFM: { // 5.1.4 - Acknowledge to GW_PASSWORD_CHANGE_REQ. id: 0x3003, req: 0x3002, // GW_PASSWORD_CHANGE_REQ decode: (data) => { checkData(data, 1) const status = data.readUInt8() if (status !== 0) { throw new Error('invalid password') } } }, GW_PASSWORD_CHANGE_NTF: { // 5.1.5 - Acknowledge to GW_PASSWORD_CHANGE_REQ. Broadcasted to all connected clients. id: 0x3004, req: 0x3002, // GW_PASSWORD_CHANGE_REQ decode: (data, session) => { checkData(data, 32) session.result = { password: data.subarray(0, data.indexOf(0)).toString() } session.emit('done') return session.result } }, // ===== 6. General Commands ================================================ GW_GET_VERSION_REQ: { // 6.1.1 - Request version information. id: 0x0008 }, GW_GET_VERSION_CFM: { // 6.1.2 Acknowledge to GW_GET_VERSION_REQ command. id: 0x0009, req: 0x0008, // GW_GET_VERSION_REQ decode (data) { checkData(data, 9) const sw = [] // for (let i = 0; i < 6; i++) { for (let i = 1; i < 5; i++) { sw.push(data.readUInt8(i)) } return { softwareVersion: sw.join('.'), hardwareVersion: data.readUInt8(6), productGroup: data.readUInt8(7), // 14 productType: data.readUInt8(8) // 3 } } }, GW_GET_PROTOCOL_VERSION_REQ: { // 6.1.3 - Request KLF 200 API protocol version. id: 0x000A }, GW_GET_PROTOCOL_VERSION_CFM: { // 6.1.4 - Acknowledge to GW_GET_PROTOCOL_VERSION_REQ command. id: 0x000B, req: 0x000A, // GW_GET_PROTOCOL_VERSION_REQ decode (data) { checkData(data, 4) return { api: data.readUint16BE(0) + '.' + data.readUInt16BE(2) } } }, GW_GET_STATE_REQ: { // 6.2.1 - Request the state of the gateway id: 0x000C }, GW_GET_STATE_CFM: { // 6.2.2 - Acknowledge to GW_GET_STATE_REQ command. id: 0x000D, req: 0x000C, // GW_GET_STATE_REQ decode (data) { checkData(data, 6) return { gatewayState: data.readUint8(0), subState: data.readUint8(1) // stateData: data.readUIntBE(32, 2) } } }, GW_LEAVE_LEARN_STATE_REQ: { // 6.3.1 - Request gateway to leave learn state. id: 0x000E }, GW_LEAVE_LEARN_STATE_CFM: { // 6.3.2 - Acknowledge to GW_LEAVE_LEARN_STATE_REQ command. id: 0x000F, req: 0x000E, // GW_LEAVE_LEARN_STATE_REQ decode: (data) => { return decodeStatus(data) } }, GW_SET_UTC_REQ: { // 6.4.1- Request to set UTC time. id: 0x2000, encode: () => { // Set current time. const data = Buffer.allocUnsafe(4) const utc = (new Date()).getTime() / 1000 data.writeUint32BE(utc, 0) return data } }, GW_SET_UTC_CFM: { // 6.4.2 - Acknowledge to GW_SET_UTC_REQ. id: 0x2001, req: 0x2000 // GW_SET_UTC_REQ }, GW_RTC_SET_TIME_ZONE_REQ: { // 6.4.3 - Set time zone and daylight savings rules. id: 0x2002, encode () { // Set time zone to UTC const data = Buffer.allocUnsafe(2).fill(0) data.write(':') return data } }, GW_RTC_SET_TIME_ZONE_CFM: { // 6.4.4 - Acknowledge to GW_RTC_SET_TIME_ZONE_REQ. id: 0x2003, req: 0x2002, // GW_RTC_SET_TIME_ZONE_REQ decode: (data) => { return decodeStatus(data) } }, GW_GET_LOCAL_TIME_REQ: { // 6.4.5 - Request the local time based on current time zone and daylight savings rules. id: 0x2004 }, GW_GET_LOCAL_TIME_CFM: { // 6.4.6 - Acknowledge to GW_RTC_SET_TIME_ZONE_REQ. id: 0x2005, req: 0x2004, // GW_GET_LOCAL_TIME_REQ decode: (data) => { checkData(data, 15) return { time: (new Date(data.readUint32BE(0) * 1000)).toISOString() } } }, GW_REBOOT_REQ: { // 6.5.1 - Request gateway to reboot. id: 0x0001 }, GW_REBOOT_CFM: { // 6.5.2 - Acknowledge to GW_REBOOT_REQ command. id: 0x0002, req: 0x0001 // GW_REBOOT_REQ }, GW_SET_FACTORY_DEFAULT_REQ: { // 6.6.1 - Request gateway to clear system table, scene table and set Ethernet settings to factory default. Gateway will reboot. id: 0x0003 }, GW_SET_FACTORY_DEFAULT_CFM: { // 6.6.2 - Acknowledge to GW_SET_FACTORY_DEFAULT_REQ command. id: 0x0004, req: 0x0003 // GW_SET_FACTORY_DEFAULT_REQ }, GW_GET_NETWORK_SETUP_REQ: { // 6.8.1 - Request network parameters. id: 0x00E0 }, GW_GET_NETWORK_SETUP_CFM: { // 6.8.2 - Acknowledge to GW_GET_NETWORK_SETUP_REQ. id: 0x00E1, req: 0x00E0, // GW_GET_NETWORK_SETUP_REQ decode: (data) => { checkData(data, 13) return { address: decodeIpv4(data.readUInt32BE(0)), mask: decodeIpv4(data.readUInt32BE(4)), gateway: decodeIpv4(data.readUInt32BE(8)), dhcp: data.readUInt8(12) !== 0 } } }, GW_SET_NETWORK_SETUP_REQ: { // 6.9.1 - Set network parameters. id: 0x00E2, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toHost('params.address', params.address, userInput) OptionParser.toHost('params.mask', params.mask, userInput) OptionParser.toHost('params.gateway', params.gateway, userInput) OptionParser.toBool('params.dhcp', params.dhcp, userInput) const data = Buffer.allocUnsafe(31).fill(0) data.writeUInt32BE(encodeIpv4(params.address, 0)) data.writeUInt32BE(encodeIpv4(params.mask, 4)) data.writeUInt32BE(encodeIpv4(params.gateway, 8)) data.writeUInt8(params.dhcp ? 1 : 0) return data }, cfm: 0x00E3 // GW_SET_NETWORK_SETUP_CFM }, GW_SET_NETWORK_SETUP_CFM: { // 6.9.2 - Acknowledge to GW_SET_NETWORK_SETUP_REQ. id: 0x00E3, req: 0x00E2 // GW_SET_NETWORK_SETUP_REQ }, GW_ERROR_NTF: { // 6.10 - Provides information on what triggered the error. id: 0x0000, decode: (data) => { checkData(data, 1) const error = data.readUInt8(0) const message = { 0: 'generic error', 1: 'invalid command', 2: 'invalid frame', 7: 'busy - try again later', 8: 'invalid node', 12: 'not authenticated' }[error] ?? 'error ' + error throw new Error(message) } }, // ===== 7. Configuration Service =========================================== GW_CS_GET_SYSTEMTABLE_DATA_REQ: { // 7.2 - Request a list of nodes in the gateways system table. id: 0x0100, ntf: true }, GW_CS_GET_SYSTEMTABLE_DATA_CFM: { // 7.3 - Acknowledge to GW_CS_GET_SYSTEMTABLE_DATA_REQ id: 0x0101, req: 0x0100 // GW_CS_GET_SYSTEMTABLE_DATA_REQ }, GW_CS_GET_SYSTEMTABLE_DATA_NTF: { // 7.4 - Acknowledge to GW_CS_GET_SYSTEM_TABLE_DATA_REQList of nodes in the gateways systemtable. id: 0x0102, req: 0x0100, // GW_CS_GET_SYSTEMTABLE_DATA_REQ decode: (data, session) => { const result = [] const nEntries = data.readUInt8(0) checkData(data, 2 + nEntries * 11) for (let i = 0; i < nEntries; i++) { const entry = { nodeId: data.readUInt8(11 * i + 1), // address: '0x' + toHexString( // data.readUInt8(11 * i + 2) << 16 | // data.readUInt8(11 * i + 3) << 8 | // data.readUInt8(11 * i + 4), 6 // ), actuatorType: data.readUInt16BE(11 * 1 + 5), // actuatorType: decodeType(data.readUInt16BE(11 * 1 + 5)), powerState: decodePowerState(data.readUInt8(11 * i + 7)), manufacturer: decodeManufacturerId(data.readUInt8(11 * i + 8)) // backbone: '0x' + toHexString( // data.readUInt8(11 * i + 9) << 16 | // data.readUInt8(11 * i + 10) << 8 | // data.readUInt8(11 * i + 11), 6 // ) } entry.model = decodeActuatorType(entry.actuatorType) result.push(entry) session.result.push(entry) } if (data.readUInt8(nEntries * 11 + 1) === 0) { // remainingNEntries session.emit('done') } return result } }, GW_CS_DISCOVER_NODES_REQ: { // 7.5.1 - Start CS DiscoverNodes macro in KLF200. id: 0x0103, ntf: true }, GW_CS_DISCOVER_NODES_CFM: { // 7.5.2 - Acknowledge to GW_CS_DISCOVER_NODES_REQ command. id: 0x0104, req: 0x0103 // GW_CS_DISCOVER_NODES_REQ }, GW_CS_DISCOVER_NODES_NTF: { // 7.5.3 - Acknowledge to GW_CS_DISCOVER_NODES_REQ command. id: 0x0105, req: 0x0103 // GW_CS_DISCOVER_NODES_REQ }, GW_CS_REMOVE_NODES_REQ: { // 7.6.1 - Remove one or more nodes in the systemtable. id: 0x0106 }, GW_CS_REMOVE_NODES_CFM: { // 7.6.2 - Acknowledge to GW_CS_REMOVE_NODES_REQ. id: 0x0107, req: 0x0106 // GW_CS_REMOVE_NODES_REQ }, GW_CS_VIRGIN_STATE_REQ: { // 7.7.1 - Clear systemtable and delete system key. id: 0x0108 }, GW_CS_VIRGIN_STATE_CFM: { // 7.7.2 - Acknowledge to GW_CS_VIRGIN_STATE_REQ. id: 0x0109, req: 0x0108 // GW_CS_VIRGIN_STATE_REQ }, GW_CS_CONTROLLER_COPY_REQ: { // 7.8.1 - Setup KLF200 to get or give a system to or from another io-homecontrol® remote control. By a system means all nodes in the systemtable and the system key. id: 0x010A, ntf: true }, GW_CS_CONTROLLER_COPY_CFM: { // 7.8.2 - Acknowledge to GW_CS_CONTROLLER_COPY_REQ. id: 0x010B, req: 0x010A // GW_CS_CONTROLLER_COPY_REQ }, GW_CS_CONTROLLER_COPY_NTF: { // 7.8.3 - Acknowledge to GW_CS_CONTROLLER_COPY_REQ. id: 0x010C, req: 0x010A // GW_CS_CONTROLLER_COPY_REQ }, GW_CS_CONTROLLER_COPY_CANCEL_NTF: { // 7.8.4 - Cancellation of system copy to other controllers. id: 0x010D, req: 0x010A // GW_CS_CONTROLLER_COPY_REQ }, GW_CS_GENERATE_NEW_KEY_REQ: { // 7.9.1 - Generate new system key and update actuators in systemtable. id: 0x0113, ntf: true }, GW_CS_GENERATE_NEW_KEY_CFM: { // 7.9.2 - Acknowledge to GW_CS_GENERATE_NEW_KEY_REQ. id: 0x0114, req: 0x0113 // GW_CS_GENERATE_NEW_KEY_REQ }, GW_CS_GENERATE_NEW_KEY_NTF: { // 7.9.3 - Acknowledge to GW_CS_GENERATE_NEW_KEY_REQ with status. id: 0x0115, req: 0x0113 // GW_CS_GENERATE_NEW_KEY_REQ }, GW_CS_RECEIVE_KEY_REQ: { // 7.10.1 - Receive system key from another controller. id: 0x010E, ntf: true }, GW_CS_RECEIVE_KEY_CFM: { // 7.10.2 - Acknowledge to GW_CS_RECEIVE_KEY_REQ. id: 0x010F, req: 0x010E // GW_CS_RECEIVE_KEY_REQ }, GW_CS_RECEIVE_KEY_NTF: { // 7.10.3 - Acknowledge to GW_CS_RECEIVE_KEY_REQ with status. id: 0x0110, req: 0x010E // GW_CS_RECEIVE_KEY_REQ }, GW_CS_REPAIR_KEY_REQ: { // 7.11.1 - Update key in actuators holding an old key. id: 0x0116, ntf: true }, GW_CS_REPAIR_KEY_CFM: { // 7.11.2 - Acknowledge to GW_CS_REPAIR_KEY_REQ. id: 0x0117, req: 0x0116 // GW_CS_REPAIR_KEY_REQ }, GW_CS_REPAIR_KEY_NTF: { // 7.11.3 - Acknowledge to GW_CS_REPAIR_KEY_REQ with status. id: 0x0118, req: 0x0116 // GW_CS_REPAIR_KEY_REQ }, GW_CS_PGC_JOB_NTF: { // 7.12.4 - Information on Product Generic Configuration job initiated by press on PGC button. id: 0x0111 }, GW_CS_SYSTEM_TABLE_UPDATE_NTF: { // 7.13.1 - Broadcasted to all clients and gives information about added and removed actuator nodes in system table. id: 0x0112 }, GW_CS_ACTIVATE_CONFIGURATION_MODE_REQ: { // 7.14.1 - Request one or more actuator to open for configuration. id: 0x0119 }, GW_CS_ACTIVATE_CONFIGURATION_MODE_CFM: { // 7.14.2 - Acknowledge to GW_CS_ACTIVATE_CONFIGURATION_MODE_REQ. id: 0x011A, req: 0x0119 // GW_CS_ACTIVATE_CONFIGURATION_MODE_REQ }, // ===== 8. Information Service ============================================= GW_HOUSE_STATUS_MONITOR_ENABLE_REQ: { // 8.2.1 - Enable house status monitor. id: 0x0240 }, GW_HOUSE_STATUS_MONITOR_ENABLE_CFM: { // 8.2.2 - Acknowledge to GW_HOUSE_STATUS_MONITOR_ENABLE_REQ. id: 0x0241, req: 0x0240 // GW_HOUSE_STATUS_MONITOR_ENABLE_REQ }, GW_HOUSE_STATUS_MONITOR_DISABLE_REQ: { // 8.2.3 - Disable house status monitor. id: 0x0242 }, GW_HOUSE_STATUS_MONITOR_DISABLE_CFM: { // 8.2.4 - Acknowledge to GW_HOUSE_STATUS_MONITOR_DISABLE_REQ. id: 0x0243, req: 0x0242 // GW_HOUSE_STATUS_MONITOR_DISABLE_REQ }, GW_GET_NODE_INFORMATION_REQ: { // 8.3.1 - Request extended information of one specific actuator node. id: 0x0200, ntf: true, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.nodeId', params.nodeId, 0, 199, userInput) const data = Buffer.allocUnsafe(1) data.writeUInt8(params.nodeId) return data } }, GW_GET_NODE_INFORMATION_CFM: { // 8.3.2 - Acknowledge to GW_GET_NODE_INFORMATION_REQ. id: 0x0201, req: 0x0200, // GW_GET_NODE_INFORMATION_REQ decode: (data, session) => { return decodeStatusId(data) } }, GW_GET_NODE_INFORMATION_NTF: { // 8.3.3 - Acknowledge to GW_GET_NODE_INFORMATION_REQ. id: 0x0210, req: 0x0200, // GW_GET_NODE_INFORMATION_REQ decode: (data, session) => { session.result = decodeNodeInformation(data) session.emit('done') return session.result } }, GW_SET_NODE_VARIATION_REQ: { // 8.3.4 - Set node variation. id: 0x0206, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.nodeId', params.nodeId, 0, 199, userInput) OptionParser.toNumber('params.nodeVariation', params.nodeVariation, 0, 4, userInput) const data = Buffer.allocUnsafe(2) data.writeUInt8(params.nodeId, 0) data.writeUInt8(params.nodeVariation, 1) return data } }, GW_SET_NODE_VARIATION_CFM: { // 8.3.5 - Acknowledge to GW_SET_NODE_VARIATION_REQ. id: 0x0207, req: 0x0206, // GW_SET_NODE_VARIATION_REQ decode: (data, session) => { return decodeStatusId(data) } }, GW_SET_NODE_NAME_REQ: { // 8.3.6 - Set node name. id: 0x0208, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.nodeId', params.nodeId, 0, 199, userInput) OptionParser.toString('params.name', params.name, true, userInput) const data = Buffer.allocUnsafe(65).fill(0) data.writeUInt8(params.nodeId, 0) data.write(params.name, 1) return data } }, GW_SET_NODE_NAME_CFM: { // 8.3.7 - Acknowledge to GW_SET_NODE_NAME_REQ. id: 0x0209, req: 0x0208, // GW_SET_NODE_NAME_REQ decode: (data, session) => { return decodeStatusId(data) } }, GW_NODE_INFORMATION_CHANGED_NTF: { // 8.3.8 - Information has been updated. id: 0x020C, decode: (data) => { return { nodeId: data.readUInt8(0), name: data.subarray(1, data.indexOf(0, 1)).toString(), order: data.readUInt16BE(65), placement: data.readUInt8(67), nodeVariation: data.readUInt8(68) } } }, GW_NODE_STATE_POSITION_CHANGED_NTF: { // 8.3.9 - Information has been updated. // The timeStamp is corrupted: The lowest 2 bytes of the 4 bytes are sent to the // higher 2 bytes and the lowest 2 bytes are 0. // Also seeing a lot of checksum errors on this message. id: 0x0211, decode: (data) => { return { nodeId: data.readUInt8(0), state: data.readUInt8(1), currentPosition: decodePosition(data.readUInt16BE(2)), targetPosition: decodePosition(data.readUInt16BE(4)), fp1Position: decodePosition(data.readUint16BE(6)), fp2Position: decodePosition(data.readUint16BE(8)), fp3Position: decodePosition(data.readUint16BE(10)), fp4Position: decodePosition(data.readUint16BE(12)), remainingTime: data.readUint16BE(14), timeStamp: new Date(data.readUint32BE(16) * 1000).toISOString() } } }, GW_GET_ALL_NODES_INFORMATION_REQ: { // 8.3.10 - Request extended information of all nodes. id: 0x0202, ntf: true }, GW_GET_ALL_NODES_INFORMATION_CFM: { // 8.3.11 - Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ id: 0x0203, req: 0x0202, // GW_GET_ALL_NODES_INFORMATION_CFM decode: (data, session) => { checkData(data, 2) if (data.readUInt8(0) !== 0) { // status throw new Error('system table empty') } return { nNodes: data.readUInt8(1) } } }, GW_GET_ALL_NODES_INFORMATION_NTF: { // 8.3.12 - Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ. Holds node information id: 0x0204, req: 0x0202, // GW_GET_ALL_NODES_INFORMATION_CFM decode: (data, session) => { const payload = decodeNodeInformation(data) session.result.push(payload) return payload } }, GW_GET_ALL_NODES_INFORMATION_FINISHED_NTF: { // 8.3.13 - Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ. No more nodes. id: 0x0205, req: 0x0202, // GW_GET_ALL_NODES_INFORMATION_CFM decode: (data, session) => { session.emit('done') } }, GW_SET_NODE_ORDER_AND_PLACEMENT_REQ: { // 8.3.14 - Set search order and room placement. id: 0x020D, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.nodeId', params.nodeId, 0, 199, userInput) OptionParser.toNumber('params.order', params.order, 0, 4, userInput) OptionParser.toNumber('params.placement', params.placement, 0, 99, userInput) const data = Buffer.allocUnsafe(4) data.writeUInt8(params.nodeId, 0) data.writeUInt16BE(params.order, 1) data.writeUInt8(params.placement, 3) return data } }, GW_SET_NODE_ORDER_AND_PLACEMENT_CFM: { // 8.3.15 - Acknowledge to GW_SET_NODE_ORDER_AND_PLACEMENT_REQ. id: 0x020E, req: 0x020D, // GW_SET_NODE_ORDER_AND_PLACEMENT_REQ decode: (data, session) => { return decodeStatusId(data) } }, GW_GET_GROUP_INFORMATION_REQ: { // 8.4.1 - Request information about all defined groups. id: 0x0220, ntf: true, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.groupId', params.groupId, 0, 99, userInput) const data = Buffer.allocUnsafe(1) data.writeUInt8(params.groupId) return data } }, GW_GET_GROUP_INFORMATION_CFM: { // 8.4.2 - Acknowledge to GW_GET_GROUP_INFORMATION_REQ. id: 0x0221, req: 0x0220, // GW_GET_GROUP_INFORMATION_REQ decode: (data, session) => { decodeStatusId(data, 'groupId') } }, GW_GET_GROUP_INFORMATION_NTF: { // 8.4.3 - Acknowledge to GW_GET_GROUP_INFORMATION_REQ. id: 0x0230, req: 0x0220, // GW_GET_GROUP_INFORMATION_REQ decode: (data, session) => { session.result = decodeGroupInformation(data) session.emit('done') return session.result } }, GW_NEW_GROUP_REQ: { // 8.4.4 - Request new group to be created. id: 0x0227, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toString('params.name', params.name, true, userInput) OptionParser.toString('params.velocity', params.velocity, true, userInput) OptionParser.toArray('params.nodeIds', params.nodeIds, userInput) if (params.nodeIds.length < 2) { throw new Error('group must contain at least 2 nodes') } const data = Buffer.allocUnsafe(96).fill(0) // data.writeUInt16BE(order, 0) // data.writeUInt16BE(placement, 2) data.write(params.name, 3) data.writeUint8(encodeVelocity(params.velocity), 67) // data.writeUint8(nodeVariation, 68) // data.writeUint8(groupType, 69) // 0: user, 1: room, 2: house, 3: all - only user seems to work data.writeUint8(params.nodeIds.length, 70) for (const i in params.nodeIds) { const nodeId = params.nodeIds[i] OptionParser.toInt('params.nodeIds[' + i + ']', nodeId, 0, 199, userInput) data[71 + (nodeId & 0xF0)] |= 1 << (nodeId & 0x0F) } return data } }, GW_NEW_GROUP_CFM: { // 8.4.5 - Acknowledge to GW_NEW_GROUP_REQ. id: 0x0228, req: 0x0227, // GW_NEW_GROUP_REQ decode: (data) => { const status = data.readUInt8() switch (status) { case 0: return { groupId: data.readUint8(1) } case 1: throw new Error('request failed') case 2: throw new Error('invalid parameter') default: throw new Error(`error ${status}`) } } }, GW_SET_GROUP_INFORMATION_REQ: { // 8.4.6 - Change an existing group. id: 0x0222, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.groupId', params.groupId, 0, 99, userInput) OptionParser.toString('params.name', params.name, true, userInput) OptionParser.toString('params.velocity', params.velocity, true, userInput) OptionParser.toArray('params.nodeIds', params.nodeIds, userInput) if (params.nodeIds.length < 2) { throw new Error('group must contain at least 2 nodes') } // OptionParser.toInt('params.revision', params.revision, 0, 65535, userInput) const data = Buffer.allocUnsafe(99).fill(0) data.writeUint8(params.groupId, 0) // data.writeUInt16BE(order, 1) // data.writeUInt16BE(placement, 3) data.write(params.name, 4) data.writeUint8(encodeVelocity(params.velocity), 68) // data.writeUint8(nodeVariation, 69) // data.writeUint8(groupType, 70) // 0: user, 1: room, 2: house, 3: all - only user seems to work data.writeUint8(params.nodeIds.length, 71) for (const i in params.nodeIds) { const nodeId = params.nodeIds[i] OptionParser.toInt('params.nodeIds[' + i + ']', nodeId, 0, 199, userInput) data[72 + (nodeId & 0xF0)] |= 1 << (nodeId & 0x0F) } // data.writeUint16BE(params.revision, 97) return data } }, GW_SET_GROUP_INFORMATION_CFM: { // 8.4.7 - Acknowledge to GW_SET_GROUP_INFORMATION_REQ. id: 0x0223, req: 0x0222, // GW_SET_GROUP_INFORMATION_REQ decode: (data) => { const status = data.readUInt8() switch (status) { case 0: return { groupId: data.readUint8(1) } case 1: throw new Error('request failed') case 2: throw new Error('invalid parameter') default: throw new Error(`error ${status}`) } } }, GW_DELETE_GROUP_REQ: { // 8.4.8 - Delete a group. id: 0x0225, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.groupId', params.groupId, 0, 99, userInput) const data = Buffer.allocUnsafe(1) data.writeUInt8(params.groupId) return data } }, GW_DELETE_GROUP_CFM: { // 8.4.9 - Acknowledge to GW_DELETE_GROUP_INFORMATION_REQ. id: 0x0226, req: 0x0225, // GW_DELETE_GROUP_REQ decode: (data, session) => { decodeStatusId(data, 'groupId') } }, GW_GROUP_DELETED_NTF: { // 8.4.10 - GW_GROUP_DELETED_NTF is broadcasted to all, when a group has been removed. // GW_GROUP_INFORMATION_CHANGED_NTF is sent instead id: 0x022D, req: 0x0225, // GW_DELETE_GROUP_REQ decode: (data, session) => { session?.emit('done') return { groupId: data.readUInt8() } } }, GW_GET_ALL_GROUPS_INFORMATION_REQ: { // 8.4.11 - Request information about all defined groups. id: 0x0229, ntf: true, encode: (params) => { const data = Buffer.alloc(2).fill(0) // data.writeUInt8(useFilter, 0) // filter seems to be applied always // data.writeUInt8(0, 1) // groupType: 0: user, 1: room, 2: house return data } }, GW_GET_ALL_GROUPS_INFORMATION_CFM: { // 8.4.12 - Acknowledge to GW_GET_ALL_GROUPS_INFORMATION_REQ. id: 0x022A, req: 0x0229, // GW_GET_ALL_GROUPS_INFORMATION_REQ decode: (data, session) => { checkData(data, 2) if (data.readUInt8(0) !== 0) { // status throw new Error('system table empty') } return { nGroups: data.readUInt8(1) // returns 2 + total number of groups, ignoring filter } } }, GW_GET_ALL_GROUPS_INFORMATION_NTF: { // 8.4.13 - Acknowledge to GW_GET_ALL_GROUPS_INFORMATION_REQ. id: 0x022B, req: 0x0229, // GW_GET_ALL_GROUPS_INFORMATION_REQ decode: (data, session) => { const payload = decodeGroupInformation(data) session.result.push(payload) return payload } }, GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF: { // 8.4.14 - Acknowledge to GW_GET_ALL_GROUPS_INFORMATION_REQ. id: 0x022C, req: 0x0229, // GW_GET_ALL_GROUPS_INFORMATION_REQ decode: (data, session) => { session.emit('done') } }, GW_GROUP_INFORMATION_CHANGED_NTF: { // 8.4.15 - Broadcast to all, about group information of a group has been changed. id: 0x0224, decode: (data) => { if (data.length < 2) { return } const event = data.readUInt8(0) const groupId = data.readUInt8(1) if (event === 0) { // group deleted checkData(data, 2) return { groupId, deleted: true } } else if (event === 1) { // group modified checkData(data, 100) const groupType = data.readUint8(71) const nNodes = data.readUInt8(72) const nodeIds = [] if (groupType === 0 && nNodes > 0) { // User-defined group. let mask for (let i = 0; i < 200; i++) { if (i % 8 === 0) { mask = data.readUint8(73 + i / 8) } if (mask & 0x01) { nodeIds.push(i) } mask >>= 1 } } return { groupId, // order: data.readUInt16BE(2), // placement: data.readUint8(4), name: data.subarray(5, data.indexOf(0, 5)).toString(), velocity: decodeVelocity(data.readUInt8(69)), // nodeVariation: data.readUInt8(70), // groupType, // nNodes, nodeIds // revision: data.readUInt16BE(98) } } } }, // ===== 9. Activation Log ================================================== GW_GET_ACTIVATION_LOG_HEADER_REQ: { // 9.1.1 - Request header from activation log. id: 0x0500 }, GW_GET_ACTIVATION_LOG_HEADER_CFM: { // 9.1.2 - Confirm header from activation log. id: 0x0501, req: 0x0500 // GW_GET_ACTIVATION_LOG_HEADER_REQ }, GW_CLEAR_ACTIVATION_LOG_REQ: { // 9.1.3 - Request clear all data in activation log. id: 0x0502 }, GW_CLEAR_ACTIVATION_LOG_CFM: { // 9.1.4 - Confirm clear all data in activation log. id: 0x0503, req: 0x0502 // GW_CLEAR_ACTIVATION_LOG_REQ }, GW_GET_ACTIVATION_LOG_LINE_REQ: { // 9.1.5 - Request line from activation log. id: 0x0504 }, GW_GET_ACTIVATION_LOG_LINE_CFM: { // 9.1.6 - Confirm line from activation log. id: 0x0505, req: 0x0504 // GW_GET_ACTIVATION_LOG_LINE_REQ }, GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_REQ: { // 9.1.7 - Request lines from activation log. id: 0x0507, ntf: true }, GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_NTF: { // 9.1.8 - Error log data from activation log. id: 0x0508, req: 0x0507 // GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_REQ }, GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_CFM: { // 9.1.9 Confirm lines from activation log. id: 0x0509, req: 0x0507 // GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_REQ }, GW_ACTIVATION_LOG_UPDATED_NTF: { // 9.1.10 - Confirm line from activation log. id: 0x0506 }, // ===== 10. Command Handler ================================================ GW_COMMAND_SEND_REQ: { // 10.1.1 - Send activating command direct to one or more io-homecontrol® nodes. id: 0x0300, ntf: true, session: true, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.sessionId', params.sessionId) params.nodeIds = OptionParser.toArray('params.nodeIds', params.nodeIds, userInput) const data = Buffer.allocUnsafe(66).fill(0) data.writeUInt16BE(params.sessionId, 0) data.writeUInt8(1, 2) // CommandOriginator: User data.writeUInt8(3, 3) // Priority Level: User Level 2 // data.writeUInt8(0, 4) // ParameterActive: Main Parameter // data.writeUInt8(0, 5) // FPI1 // data.writeUInt8(0, 6) // FPI1 data.writeUInt16BE(encodePosition(params.position), 7) data.writeUInt8(params.nodeIds.length, 41) for (let i = 0; i < params.nodeIds.length; i++) { OptionParser.toNumber(`params.nodeIds[${i}]`, params.nodeIds[i], 0, 199, userInput) data.writeUInt8(params.nodeIds[i], 42 + i) } // data.writeUInt8(0, 62) // PriorityLevelLock // data.writeUInt8(0, 63) // PL_0_3 // data.writeUInt8(0, 64) // PL_4_7 // data.writeUInt8(0, 65) // LockTime return data } }, GW_COMMAND_SEND_CFM: { // 10.1.2 - Acknowledge to GW_COMMAND_SEND_REQ. id: 0x0301, decode: (data) => { return decodeSessionStatus(data) } }, GW_COMMAND_RUN_STATUS_NTF: { // 10.1.3 - Gives run status for io-homecontrol® node. id: 0x0302, decode: (data) => { checkData(data, 13) return { sessionId: data.readUInt16BE(0), status: data.readUInt8(2), nodeId: data.readUInt8(3), nodeParameter: decodeNodeParameter(data.readUInt8(4)), currentPosition: decodePosition(data.readUInt16BE(5)), runStatus: decodeRunStatus(data.readUInt8(7)), statusReply: '0x' + toHexString(data.readUInt8(8), 2), informationCode: '0x' + toHexString(data.readUInt32BE(9), 8) } } }, GW_COMMAND_REMAINING_TIME_NTF: { // 10.1.4 - Gives remaining time before io-homecontrol® node enter target position. id: 0x0303, decode: (data) => { checkData(data, 6) return { sessionId: data.readUInt16BE(0), nodeId: data.readUInt8(2), nodeParameter: decodeNodeParameter(data.readUInt8(3)), duration: data.readUInt16BE(4) } } }, GW_SESSION_FINISHED_NTF: { // 10.1.5 - Command send, Status request, Wink, Mode or Stop session is finished. id: 0x0304, sessionDone: true, decode: (data) => { checkData(data, 2) return { sessionId: data.readUInt16BE(0) } } }, GW_STATUS_REQUEST_REQ: { // 10.3.1 - Get status request from one or more io-homecontrol® nodes. id: 0x0305, ntf: true, session: true, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.sessionId', params.sessionId) params.nodeIds = OptionParser.toArray('params.nodeIds', params.nodeIds, userInput) const data = Buffer.allocUnsafe(26).fill(0) data.writeUInt16BE(params.sessionId, 0) data.writeUInt8(params.nodeIds.length, 2) for (let i = 0; i < params.nodeIds.length; i++) { OptionParser.toNumber(`params.nodeIds[${i}]`, params.nodeIds[i], 0, 199, userInput) data.writeUInt8(params.nodeIds[i], 3 + i) } data.writeUInt8(3, 23) // Status Type: request main info // data.writeUInt8(0, 24) // FPI1 // data.writeUInt8(0, 25) // FPI1 return data } }, GW_STATUS_REQUEST_CFM: { // 10.3.2 - Acknowledge to GW_STATUS_REQUEST_REQ. id: 0x0306, decode: (data) => { return decodeSessionStatus(data) } }, GW_STATUS_REQUEST_NTF: { // 10.3.3 - Status request from one or more io-homecontrol® nodes. id: 0x0307, decode: (data) => { checkData(data, 18) return { sessionId: data.readUInt16BE(0), status: data.readUInt8(2), nodeId: data.readUInt8(3), runStatus: decodeRunStatus(data.readUInt8(4)), statusReply: data.readUInt8(5), statusType: data.readUInt8(6), targetPosition: decodePosition(data.readUInt16BE(7)), currentPosition: decodePosition(data.readUInt16BE(9)), remainingTime: data.readUInt16BE(11), lastMasterExecutionAddress: '0x' + toHexString(data.readUInt32BE(13), 8), lastCommandOriginator: data.readUInt8(17) } } }, GW_WINK_SEND_REQ: { // 10.4.1 - Request from one or more io-homecontrol® nodes to Wink. id: 0x0308, ntf: true, session: true, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.sessionId', params.sessionId) params.nodeIds = OptionParser.toArray('params.nodeIds', params.nodeIds, userInput) const data = Buffer.allocUnsafe(27).fill(0) data.writeUInt16BE(params.sessionId, 0) data.writeUInt8(1, 2) // CommandOriginator: User data.writeUInt8(3, 3) // Priority Level: User Level 2 data.writeUInt8(1, 4) // Wink Status: Enable Wink data.writeUInt8(254, 5) // Wink Time: Manufacturer-Specific Wink Time data.writeUInt8(params.nodeIds.length, 6) for (let i = 0; i < params.nodeIds.length; i++) { OptionParser.toNumber(`params.nodeIds[${i}]`, params.nodeIds[i], 0, 199, userInput) data.writeUInt8(params.nodeIds[i], 7 + i) } return data } }, GW_WINK_SEND_CFM: { // 10.4.2 - Acknowledge to GW_WINK_SEND_REQ id: 0x0309, decode: (data) => { return decodeSessionStatus(data) } }, GW_WINK_SEND_NTF: { // 10.4.4 - Status info for performed wink request. id: 0x030A, sessionDone: true, decode: (data) => { checkData(data, 2) return { sessionId: data.readUInt16BE(0) } } }, GW_SET_LIMITATION_REQ: { // 10.5.2 - Set a parameter limitation in an actuator. id: 0x0310, session: true }, GW_SET_LIMITATION_CFM: { // 10.5.3 - Acknowledge to GW_SET_LIMITATION_REQ. id: 0x0311, req: 0x0310 // GW_SET_LIMITATION_REQ }, GW_LIMITATION_STATUS_NTF: { // 10.5.4 - Hold information about limitation. id: 0x0314 }, GW_GET_LIMITATION_STATUS_REQ: { // 10.5.8 - Get parameter limitation in an actuator. id: 0x0312, session: true }, GW_GET_LIMITATION_STATUS_CFM: { // 10.5.9 - Acknowledge to GW_GET_LIMITATION_STATUS_REQ. id: 0x0313, req: 0x0312 // GW_GET_LIMITATION_STATUS_REQ }, GW_MODE_SEND_REQ: { // 10.6.1 - Send Activate Mode to one or more io-homecontrol® nodes. id: 0x0320 }, GW_MODE_SEND_CFM: { // 10.6.2 - Acknowledge to GW_MODE_SEND_REQ id: 0x0321, req: 0x0320 // GW_MODE_SEND_REQ }, // GW_MODE_SEND_NTF: { // (undocumented) - Notify with Mode activation info. // id: 0x0322 // // req: 0x0320 // GW_MODE_SEND_REQ // }, GW_ACTIVATE_PRODUCTGROUP_REQ: { // 10.7.1 - Activate a product group in a given direction. id: 0x0447, ntf: true, session: true, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.sessionId', params.sessionId) OptionParser.toNumber('params.groupId', params.groupId, 0, 99, userInput) OptionParser.toString('params.velocity', params.velocity, true, userInput) const data = Buffer.allocUnsafe(13).fill(0) data.writeUInt16BE(params.sessionId, 0) data.writeUInt8(1, 2) // Command Originator: User data.writeUInt8(3, 3) // Priority Level: User Level 2 data.writeUInt8(params.groupId, 4) // data.writeUInt8(0, 5) // Parameter ID: Main Parameter data.writeUInt16BE(encodePosition(params.position), 6) data.writeUInt8(encodeVelocity(params.velocity), 8) // data.writeUInt8(0, 9) // PriorityLevelLock // data.writeUInt8(0, 10) // PL_0_3 // data.writeUInt8(0, 11) // PL_4_7 // data.writeUInt8(0, 12) // LockTime return data } }, GW_ACTIVATE_PRODUCTGROUP_CFM: { // 10.7.2 - Acknowledge to GW_ACTIVATE_PRODUCTGROUP_REQ. id: 0x0448, decode: (data) => { checkData(data, 3) const status = data.readUInt8(2) if (status === 0) { return { sessionId: data.readUInt16BE(0) } } // TODO need to return both error and session const message = { 1: 'unknown groupId', 2: 'sessionId already in use', 3: 'busy - try again later', 4: 'invalid group type', 5: 'request failed', 6: 'invalid parameter' }[status] ?? 'status ' + status throw new Error(message) } }, // GW_ACTIVATE_PRODUCTGROUP_NTF: { // (undocumented) - Acknowledge to GW_ACTIVATE_PRODUCTGROUP_REQ. // id: 0x0449 // // req: 0x0447 // GW_ACTIVATE_PRODUCTGROUP_REQ // }, // ===== 11. Scenes ========================================================= GW_INITIALIZE_SCENE_REQ: { // 11.1.2 - Prepare gateway to record a scene. id: 0x0400, ntf: true }, GW_INITIALIZE_SCENE_CFM: { // 11.1.3 - Acknowledge to GW_INITIALIZE_SCENE_REQ. id: 0x0401, req: 0x0400 // GW_INITIALIZE_SCENE_REQ }, GW_INITIALIZE_SCENE_NTF: { // 11.1.4 - Acknowledge to GW_INITIALIZE_SCENE_REQ. id: 0x0402, req: 0x0400 // GW_INITIALIZE_SCENE_REQ }, GW_INITIALIZE_SCENE_CANCEL_REQ: { // 11.2.1 - Cancel record scene process. id: 0x0403 }, GW_INITIALIZE_SCENE_CANCEL_CFM: { // 11.2.2 - Acknowledge to GW_INITIALIZE_SCENE_CANCEL_REQ command. id: 0x0404, req: 0x0403 // GW_INITIALIZE_SCENE_CANCEL_REQ }, GW_RECORD_SCENE_REQ: { // 11.4.1 - Store actuator positions changes since GW_INITIALIZE_SCENE, as a scene. id: 0x0405, ntf: true }, GW_RECORD_SCENE_CFM: { // 11.4.2 - Acknowledge to GW_RECORD_SCENE_REQ. id: 0x0406, req: 0x0405 // GW_RECORD_SCENE_REQ }, GW_RECORD_SCENE_NTF: { // 11.4.3 - Acknowledge to GW_RECORD_SCENE_REQ. id: 0x0407, req: 0x0405 // GW_RECORD_SCENE_REQ }, GW_DELETE_SCENE_REQ: { // 11.5.1 - Delete a recorded scene. id: 0x0408 }, GW_DELETE_SCENE_CFM: { // 11.5.2 - Acknowledge to GW_DELETE_SCENE_REQ. id: 0x0409, req: 0x0408 // GW_DELETE_SCENE_REQ }, GW_RENAME_SCENE_REQ: { // 11.6.1 - Request a scene to be renamed. id: 0x040A }, GW_RENAME_SCENE_CFM: { // (undocumented) - Acknowledge to GW_RENAME_SCENE_REQ. id: 0x040B, req: 0x040A // GW_RENAME_SCENE_REQ }, GW_GET_SCENE_LIST_REQ: { // 11.7.1 - Request a list of scenes. id: 0x040C, ntf: true }, GW_GET_SCENE_LIST_CFM: { // 11.7.2 - Acknowledge to GW_GET_SCENE_LIST. id: 0x040D, req: 0x040C // GW_GET_SCENE_LIST_REQ }, GW_GET_SCENE_LIST_NTF: { // 11.7.3 - Acknowledge to GW_GET_SCENE_LIST. id: 0x040E, req: 0x040C // GW_GET_SCENE_LIST_REQ }, GW_GET_SCENE_INFORMATION_REQ: { // 11.8.1 - Request extended information for one given scene. id: 0x040F, ntf: true }, GW_GET_SCENE_INFORMATION_CFM: { // 11.8.2 - Acknowledge to GW_GET_SCENE_INFOAMATION_REQ. id: 0x0410, req: 0x040F // GW_GET_SCENE_INFORMATION_REQ }, GW_GET_SCENE_INFORMATION_NTF: { // 11.8.3 - Acknowledge to GW_GET_SCENE_INFOAMATION_REQ. id: 0x0411, req: 0x040F // GW_GET_SCENE_INFORMATION_REQ }, GW_SCENE_INFORMATION_CHANGED_NTF: { // 11.9.1 - A scene has either been changed or removed. id: 0x0419 }, GW_ACTIVATE_SCENE_REQ: { // 11.10.1 - Request gateway to enter a scene. id: 0x0412, session: true }, GW_ACTIVATE_SCENE_CFM: { // 11.10.2 - Acknowledge to GW_ACTIVATE_SCENE_REQ. id: 0x0413, req: 0x0412 // GW_ACTIVATE_SCENE_REQ }, GW_STOP_SCENE_REQ: { // 11.11.1 - Request all nodes in a given scene to stop at their current position. id: 0x0415, session: true }, GW_STOP_SCENE_CFM: { // 11.11.2 Acknowledge to GW_STOP_SCENE_REQ. id: 0x0416, req: 0x0415 // GW_STOP_SCENE_REQ }, // ===== 12. Contact Input ================================================== GW_SET_CONTACT_INPUT_LINK_REQ: { // 12.1.1 - Set a link from a Contact Input to a scene or product group. id: 0x0462, encode: (params, userInput = false) => { OptionParser.toObject('params', params, userInput) OptionParser.toNumber('params.inputId', params.inputId, 0, 9) const data = Buffer.allocUnsafe(17).fill(0) data.writeUInt8(params.inputId, 0) if (params.sceneId) { OptionParser.toNumber('params.sceneId', params.sceneId, 0, 99) data.writeUInt8(1, 1) // assignment: scene data.writeUInt8(params.sceneId, 2) } else if (params.groupId) { OptionParser.toNumber('params.groupId', params.groupId, 0, 99) data.writeUInt8(2, 1) // assignment: group data.writeUInt8(params.groupId, 2) } else if (params.nodeId) { OptionParser.toNumber('params.nodeId', params.nodeId, 0, 199) data.writeUInt8(3, 1) // assignment: node data.writeUInt8(params.nodeId, 2) } else { // data.writeUInt8(0, 1) // unassigned } data.writeUInt8(1, 3) // CommandOriginator: User data.writeUInt8(3, 4) // Priority Level: User Level 2 // data.writeUInt8(0, 5) // ParameterActive: Main Parameter data.writeUInt16BE(encodePosition(params.position), 6) data.writeUInt8(encodeVelocity(para