├── src ├── zigate.js ├── driver │ ├── responses │ │ ├── version.js │ │ ├── router_discovery.js │ │ ├── factory_restart.js │ │ ├── non_factory_restart.js │ │ ├── debug_1111.js │ │ ├── device_remove.js │ │ ├── log_message.js │ │ ├── debug_2222.js │ │ ├── permit_join_status.js │ │ ├── time_get.js │ │ ├── network_state.js │ │ ├── default_response.js │ │ ├── network_joined.js │ │ ├── active_endpoint.js │ │ ├── object_command_list.js │ │ ├── level_update.js │ │ ├── on_off_update.js │ │ ├── attribute_discovery.js │ │ ├── devices_list.js │ │ ├── data_confirm_fail.js │ │ ├── descriptor_power.js │ │ ├── descriptor_complex.js │ │ ├── object_cluster_list.js │ │ ├── ieee_address.js │ │ ├── object_attribute_list.js │ │ ├── status.js │ │ ├── data_indication.js │ │ ├── device_announce.js │ │ ├── zone_status_change.js │ │ ├── descriptor_simple.js │ │ ├── attribute_write.js │ │ ├── attribute_read.js │ │ ├── descriptor_node.js │ │ └── attribute_report.js │ ├── commands │ │ ├── version.js │ │ ├── network_state.js │ │ ├── factory_new_reset.js │ │ ├── start_network.js │ │ ├── start_network_scan.js │ │ ├── permit_join_status.js │ │ ├── erase_persistent_data.js │ │ ├── time_get.js │ │ ├── channel_mask.js │ │ ├── devices_list.js │ │ ├── reset.js │ │ ├── led.js │ │ ├── device_type.js │ │ ├── active_endpoint.js │ │ ├── descriptor_node.js │ │ ├── descriptor_power.js │ │ ├── certification.js │ │ ├── descriptor_complex.js │ │ ├── descriptor_simple.js │ │ ├── permit_join.js │ │ ├── device_remove.js │ │ ├── time_set.js │ │ ├── ieee_address.js │ │ ├── action_onoff.js │ │ ├── action_onoff_timed.js │ │ ├── attribute_discovery.js │ │ ├── attribute_read.js │ │ └── attribute_write.js │ ├── clusters │ │ ├── genIdentify.js │ │ ├── genGroups.js │ │ ├── genDeviceTempCfg.js │ │ ├── genPowerCfg.js │ │ └── genBasic.js │ ├── commandBuilder.js │ ├── buffer-reader.js │ ├── responseBuilder.js │ ├── driver.js │ └── enum.js └── coordinator │ ├── ziendpoint.js │ ├── zicommand.js │ ├── zicluster.js │ ├── ziattribute.js │ ├── action.js │ ├── symbols.js │ ├── event.js │ ├── value.js │ ├── device.js │ ├── deviceTypes.js │ └── deviceLoadSave.js ├── test ├── test-coordinator.js └── test-driver.js ├── doc ├── documentation on internet.txt ├── compaptible device infos.txt └── device inclusion - messages workflow.txt ├── devices ├── xiaomi.door_sensor.js ├── xiaomi.remote_wall_switch_single.js ├── xiaomi.water_sensor.js ├── xiaomi.remote_wall_switch_double.js ├── xiaomi.vibration_sensor.js ├── xiaomi.switch_key.js ├── xiaomi.motion_sensor.js ├── xiaomi.sensor_weather.js ├── xiaomi.cube_magic.js └── osram.smart_plus_plug.js ├── package.json ├── .gitignore ├── README.md └── LICENSE /src/zigate.js: -------------------------------------------------------------------------------- 1 | const Driver = require('./driver/driver.js'); 2 | const Coordinator = require('./coordinator/coordinator.js'); 3 | 4 | module.exports = { 5 | Driver: Driver, 6 | Coordinator: Coordinator, 7 | }; 8 | -------------------------------------------------------------------------------- /test/test-coordinator.js: -------------------------------------------------------------------------------- 1 | 2 | Zi = require('../'); 3 | 4 | let coord = new Zi.Coordinator({ 5 | log: 'console', 6 | port: 'auto', 7 | loadsavepath: './network_devices.json', 8 | }); 9 | 10 | coord.start(); 11 | -------------------------------------------------------------------------------- /src/driver/responses/version.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x8010, 3 | name: "version", 4 | parse: function(reader, rep, version) { 5 | rep.major = reader.nextUInt16BE(); 6 | rep.installer = reader.nextUInt16BE(); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/driver/responses/router_discovery.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x8701, 3 | name: "router_discovery", 4 | parse: function(reader, rep, version) { 5 | rep.status = reader.nextUInt8(); 6 | rep.networkStatus = reader.nextUInt8(); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/driver/responses/factory_restart.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8007, 5 | name: "factory_restart", 6 | parse: function(reader, rep, version) { 7 | rep.status = Enum.RESTART_STATUS(reader.nextUInt8()); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/driver/responses/non_factory_restart.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8006, 5 | name: "non_factory_restart", 6 | parse: function(reader, rep, version) { 7 | rep.status = Enum.RESTART_STATUS(reader.nextUInt8(0)); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/driver/responses/debug_1111.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x1111, 3 | name: "debug_1111", 4 | parse: function(reader, rep, version) { 5 | // console.error("debug message 0x1111 received. Payload=("+payload.toString('hex').replace(/../g, "$& ")+")"); 6 | rep.data = reader.restAll(); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/driver/responses/device_remove.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8048, 5 | name: "device_remove", 6 | parse: function(reader, rep, version) { 7 | rep.ieee = reader.nextBuffer(8).toString('hex'); 8 | rep.rejoin = !!(reader.nextUInt8()); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/driver/responses/log_message.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8001, 5 | name: "log_message", 6 | parse: function(reader, rep, version) { 7 | rep.level = Enum.LOG_LEVEL(reader.nextUInt8()); 8 | rep.message = reader.restAll().toString(); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/driver/responses/debug_2222.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x2222, 3 | name: "debug_2222", 4 | parse: function(reader, rep, version) { 5 | // console.error("debug message 0x2222 received. Payload=("+payload.toString('hex').replace(/../g, "$& ")+")"); 6 | rep.data = reader.restAll(); 7 | 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/driver/responses/permit_join_status.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8014, 5 | name: "permit_join_status", 6 | parse: function(reader, rep, version) { 7 | rep.status = Enum.PERMIT_JOIN_STATUS(reader.nextUInt8()); 8 | rep.enabled = rep.status.id ? true : false; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/driver/commands/version.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0010, 3 | name: "version", 4 | statusExpected: true, 5 | responseExpected: 'version', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.payload = Buffer.alloc(0); 11 | return cmd; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/commands/network_state.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0009, 3 | name: 'network_state', 4 | statusExpected: true, 5 | responseExpected: 'network_state', 6 | minVersion: 781, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | build: function(options, cmd, version) { 9 | cmd.payload = Buffer.alloc(0); 10 | return cmd; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/driver/responses/time_get.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | const TIME_BASE = new Date('2000-01-01 00:00:00Z').getTime(); 3 | 4 | module.exports = { 5 | id: 0x8017, 6 | name: "time_get", 7 | parse: function(reader, rep, version) { 8 | rep.timestamp = reader.nextUInt32(); 9 | rep.time = new Date(TIME_BASE + rep.timestamp*1000); 10 | 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/driver/commands/factory_new_reset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0013, 3 | statusExpected: true, 4 | responseExpected: false, 5 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 6 | 7 | 8 | name: "factory_new_reset", 9 | build: function(options, cmd, version) { 10 | cmd.payload = Buffer.alloc(0); 11 | return cmd; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/commands/start_network.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0024, 3 | name: 'start_network', 4 | statusExpected: true, 5 | responseExpected: 'network_joined', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.payload = Buffer.alloc(0); 11 | return cmd; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/commands/start_network_scan.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0025, 3 | name: "start_network_scan", 4 | statusExpected: true, 5 | responseExpected: false, 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.payload = Buffer.alloc(0); 11 | return cmd; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/commands/permit_join_status.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0014, 3 | name: "permit_join_status", 4 | statusExpected: true, 5 | responseExpected: "permit_join_status", 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.payload = Buffer.alloc(0); 11 | return cmd; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/commands/erase_persistent_data.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0012, 3 | name: "erase_persistent_data", 4 | statusExpected: false, 5 | responseExpected: 'factory_restart', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.payload = Buffer.alloc(0); 11 | return cmd; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/commands/time_get.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x0017, 5 | name: "time_get", 6 | statusExpected: true, 7 | responseExpected: 'time_get', 8 | minVersion: 783, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 9 | 10 | build: function(options, cmd, version) { 11 | cmd.payload = Buffer.alloc(0); 12 | return cmd; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/driver/commands/channel_mask.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0021, 3 | name: "channel_mask", 4 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 5 | 6 | 7 | build: function(options, cmd, version) { 8 | cmd.mask = options.mask || 11; 9 | 10 | cmd.payload = Buffer.alloc(4); 11 | 12 | cmd.payload.writeUInt32BE(cmd.mask) 13 | 14 | return cmd; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/driver/commands/devices_list.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x0015, 5 | name: "devices_list", 6 | statusExpected: true, 7 | responseExpected: 'devices_list', 8 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 9 | 10 | build: function(options, cmd, version) { 11 | cmd.payload = Buffer.alloc(0); 12 | return cmd; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /doc/documentation on internet.txt: -------------------------------------------------------------------------------- 1 | ZigBee summary: 2 | https://www.mwrinfosecurity.com/assets/Whitepapers/mwri-zigbee-overview-finalv2.pdf 3 | 4 | 5 | Zigbee (full) spec book: 6 | https://www3.nd.edu/~mhaenggi/ee67011/zigbee.pdf 7 | 8 | 9 | ZCL frames & context: 10 | https://mmbnetworks.atlassian.net/wiki/spaces/SPRHA17/pages/99319820/Frame+Payload+Definitions#FramePayloadDefinitions-ZDODeviceAnnounceReceived 11 | -------------------------------------------------------------------------------- /src/driver/commands/reset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0011, 3 | name: "reset", 4 | statusExpected: false, // bug in frame_end just after reset ; status frame is discarded */ 5 | responseExpected: 'non_factory_restart', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.payload = Buffer.alloc(0); 11 | return cmd; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/commands/led.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x0018, 5 | name: "led", 6 | statusExpected: true, 7 | responseExpected: false, 8 | minVersion: 783, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 9 | 10 | 11 | build: function(options, cmd, version) { 12 | cmd.payload = Buffer.alloc(1); 13 | cmd.payload.writeUInt8(cmd.led ? 0 : 1) 14 | return cmd; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/driver/responses/network_state.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8009, 5 | name: "network_state", 6 | parse: function(reader, rep, version) { 7 | rep.address = reader.nextUInt16BE(); 8 | rep.ieee = reader.nextBuffer(8).toString('hex'); 9 | rep.panid = reader.nextUInt16BE(); 10 | rep.ieeepanid = reader.nextBuffer(8).toString('hex'); 11 | rep.channel = reader.nextUInt8(); 12 | rep.networkUp = (rep.panid !== 0); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/driver/responses/default_response.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8101, 5 | name: "default_response", 6 | parse: function(reader, rep, version) { 7 | rep.sequence = reader.nextUInt8(); 8 | rep.endpoint = reader.nextUInt8(); 9 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE(), new Error("default_response: unknown cluster")); 10 | rep.command = reader.nextUInt8(); 11 | rep.status = Enum.COMMAND_STATUS(reader.nextUInt8()); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/driver/responses/network_joined.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | // 01 / 80 24 / 00 0d / b3 / 01 00 00 00 15 8d 00 01 b2 2e 15 0b 00 4 | 5 | module.exports = { 6 | id: 0x8024, 7 | name: "network_joined", 8 | parse: function(reader, rep, version) { 9 | rep.status = Enum.NETWORK_JOIN_STATUS(reader.nextUInt8()); 10 | rep.address = reader.nextUInt16BE(); 11 | rep.ieee = reader.nextBuffer(8).toString('hex'); 12 | rep.channel = reader.nextUInt8(); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/driver/commands/device_type.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x0023, 5 | name: "device_type", 6 | statusExpected: true, 7 | responseExpected: false, 8 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 9 | 10 | 11 | build: function(options, cmd, version) { 12 | cmd.type = Enum.DEVICE_TYPE(options.type, new Error("unknown device type '"+options.type+"'")); 13 | 14 | cmd.payload = Buffer.alloc(1); 15 | 16 | cmd.payload.writeUInt8(cmd.type.id) 17 | return cmd; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/driver/responses/active_endpoint.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8045, 5 | name: "active_endpoint", 6 | parse: function(reader, rep, version) { 7 | rep.sequence = reader.nextUInt8(); 8 | rep.status = Enum.COMMAND_STATUS(reader.nextUInt8(), new Error("active_endpoint: unknown status")); 9 | rep.address = reader.nextUInt16BE(); 10 | 11 | rep.endpoints = []; 12 | var count = reader.nextUInt8(); 13 | for (var i=0; i { return {id: id, name: 'unknown profile '+id}; }); 10 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE()); 11 | 12 | rep.commands = []; 13 | while (reader.isMore()) { 14 | rep.commands.push(reader.nextUInt8()); 15 | } 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/driver/responses/level_update.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x8085, 3 | name: "level_update", 4 | parse: function(reader, rep) { 5 | rep.sequence = reader.nextUInt8(); 6 | rep.endpoint = reader.nextUInt8(); 7 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE(), new Error("data_indication: unknown cluster")); 8 | rep.addressMode = Enum.ADDRESS_MODE(reader.nextUInt8()); 9 | rep.address = rep.addressMode.name === 'short' ? reader.nextUInt16BE() : reader.nextBuffer(8).toString('hex'); 10 | rep.status = Enum.COMMAND_STATUS(reader.nextUInt8()); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/driver/responses/on_off_update.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x8095, 3 | name: "on_off_update", 4 | parse: function(reader, rep) { 5 | rep.sequence = reader.nextUInt8(); 6 | rep.endpoint = reader.nextUInt8(); 7 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE(), new Error("data_indication: unknown cluster")); 8 | rep.addressMode = Enum.ADDRESS_MODE(reader.nextUInt8()); 9 | rep.address = rep.addressMode.name === 'short' ? reader.nextUInt16BE() : reader.nextBuffer(8).toString('hex'); 10 | rep.status = Enum.COMMAND_STATUS(reader.nextUInt8()); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/driver/commands/active_endpoint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0045, 3 | name: "active_endpoint", 4 | statusExpected: true, 5 | responseExpected: 'active_endpoint', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 11 | 12 | cmd.payload = Buffer.alloc(2); 13 | cmd.payload.writeUInt16BE(cmd.address, 0); 14 | return cmd; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/driver/commands/descriptor_node.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0042, 3 | name: "descriptor_node", 4 | statusExpected: true, 5 | responseExpected: 'descriptor_node', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | build: function(options, cmd, version) { 9 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 10 | 11 | cmd.payload = Buffer.alloc(2); 12 | cmd.payload.writeUInt16BE(parseInt(cmd.address), 0); 13 | return cmd; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/driver/commands/descriptor_power.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0044, 3 | name: "descriptor_power", 4 | statusExpected: true, 5 | responseExpected: 'descriptor_power', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | build: function(options, cmd, version) { 9 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 10 | 11 | cmd.payload = Buffer.alloc(2); 12 | cmd.payload.writeUInt16BE(parseInt(cmd.address), 0); 13 | return cmd; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/driver/responses/attribute_discovery.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8140, 5 | name: "attribute_discovery", 6 | parse: function(reader, rep, version) { 7 | rep.complete = !!(reader.nextUInt8()); 8 | rep.type = Enum.ATTRIBUTE_TYPE(reader.nextUInt8(), new Error('unknown attribute type ')); 9 | rep.id = reader.nextUInt16BE(); 10 | 11 | if (version >= 783 /*3.0f*/ ) { 12 | rep.address = reader.nextUInt16BE(); 13 | rep.endpoint = reader.nextUInt8(); 14 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE()); 15 | } 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/driver/responses/devices_list.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8015, 5 | name: "devices_list", 6 | parse: function(reader, rep, version) { 7 | rep.devices = []; 8 | while(reader.isMore()) { 9 | let device = {}; 10 | device.id = reader.nextUInt8(); 11 | device.address = reader.nextUInt16BE(); 12 | device.ieee = reader.nextBuffer(8).toString('hex'); 13 | device.battery = reader.nextUInt8() === 0; 14 | device.linkQuality = reader.nextUInt8(); 15 | 16 | rep.devices.push(device); 17 | } 18 | return rep; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/driver/commands/certification.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x0019, 5 | name: "certification", 6 | statusExpected: true, 7 | responseExpected: false, 8 | minVersion: 783, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 9 | 10 | 11 | build: function(options, cmd, version) { 12 | 13 | cmd.certification = Enum.CERTIFICATION(options.certification, new Error("unknown certification '"+options.certification+"'")); 14 | 15 | cmd.payload = Buffer.alloc(1); 16 | cmd.payload.writeUInt8(cmd.certification.id) 17 | return cmd; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/driver/responses/data_confirm_fail.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8702, 5 | name: "data_confirm_fail", 6 | parse: function(reader, rep, version) { 7 | rep.failstatus = reader.nextUInt8(); 8 | rep.endpointSource = reader.nextUInt8(); 9 | rep.endpoint = reader.nextUInt8(); 10 | rep.addressMode = Enum.ADDRESS_MODE(reader.nextUInt8()); 11 | if (rep.addressMode.name === 'short') { 12 | rep.address = reader.nextUInt16BE(); 13 | } 14 | else { 15 | rep.ieee = reader.nextBuffer(8).toString('hex'); 16 | } 17 | rep.sequence = reader.nextUInt8(); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/driver/responses/descriptor_power.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x8044, 3 | name: "descriptor_power", 4 | 5 | 6 | parse: function(reader, rep, version) { 7 | rep.sequence = reader.nextUInt8(); 8 | rep.status = reader.nextUInt8(); 9 | var flags = reader.nextUInt16BE(); 10 | 11 | rep.powerMode = flags & 0x07; // 0 to 3: current power mode 12 | rep.availableSource = (flags >> 3) & 0x0F; // 4 to 7: available power source 13 | rep.currentSource = (flags >> 7) & 0x0F; // 8 to 11: current power source 14 | rep.currentLevel = (flags >> 12) & 0x0F; // 12 to 15: current power source level 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/driver/responses/descriptor_complex.js: -------------------------------------------------------------------------------- 1 | // parsed response example: 2 | // descriptor_complex(0x8034), sequence:117, status:132, interest:0, xmlTag:0, values:[], rssi:108 3 | 4 | module.exports = { 5 | id: 0x8034, 6 | name: "descriptor_complex", 7 | parse: function(reader, rep, version) { 8 | rep.sequence = reader.nextUInt8(); 9 | rep.status = reader.nextUInt8(); 10 | rep.interest = reader.nextUInt16BE(); 11 | rep.xmlTag = reader.nextUInt8(); 12 | 13 | rep.values = []; 14 | var fieldCount = reader.nextUInt8(); 15 | for(let i=0; i { return {id: id, name: 'unknown profile '+id}; }); 11 | 12 | rep.clusters = []; 13 | while(reader.isMore()) { 14 | rep.clusters.push( 15 | Enum.CLUSTERS(reader.nextUInt16BE()) 16 | ); 17 | } 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/driver/responses/ieee_address.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x8041, 5 | name: "ieee_address", 6 | 7 | 8 | parse: function(reader, rep, version) { 9 | rep.sequence = reader.nextUInt8(); 10 | rep.status = Enum.COMMAND_STATUS(reader.nextUInt8(), new Error("ieee_address: unknown status")); 11 | rep.ieee = reader.nextBuffer(8).toString('hex'); 12 | rep.address = reader.nextUInt16BE(); 13 | let count = reader.nextUInt8(); 14 | rep.start = reader.nextUInt8(); 15 | 16 | rep.devices = []; 17 | for (let i=0; i{throw new Error("invalid parameter 'address'.");})(); 8 | cmd.interest = !(isNaN(parseInt(options.interest))) ? options.interest : (()=>{throw new Error("invalid parameter 'interest'.");})(); 9 | 10 | cmd.payload = Buffer.alloc(4); 11 | cmd.payload.writeUInt16BE(parseInt(cmd.address), 0); 12 | cmd.payload.writeUInt16BE(parseInt(cmd.interest), 0); 13 | return cmd; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/driver/responses/object_attribute_list.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | // 01 80 04 00 1c df 01 01 04 00 00 00 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07 40 00 ff 01 ff 02 00 4 | // 01 80 04 00 08 8b 01 01 04 00 03 00 00 00 5 | 6 | module.exports = { 7 | id: 0x8004, 8 | name: "object_attribute_list", 9 | parse: function(reader, rep, version) { 10 | rep.endpoint = reader.nextUInt8(); 11 | rep.profile = Enum.PROFILES(reader.nextUInt16BE(), (id) => { return {id: id, name: 'unknown profile '+id}; }); 12 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE(3)); 13 | 14 | rep.attributes = [] 15 | while(reader.isMore()) { 16 | rep.attributes.push(reader.nextUInt16BE()); 17 | } 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/driver/commands/descriptor_simple.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0043, 3 | name: "descriptor_simple", 4 | statusExpected: true, 5 | responseExpected: 'descriptor_simple', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 11 | cmd.endpoint = !(isNaN(parseInt(options.endpoint))) ? options.endpoint : (()=>{throw new Error("invalid parameter 'endpoint'.");})(); 12 | 13 | cmd.payload = Buffer.alloc(3); 14 | cmd.payload.writeUInt16BE(parseInt(cmd.address), 0); 15 | cmd.payload.writeUInt8(parseInt(cmd.endpoint), 2); 16 | return cmd; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/driver/commands/permit_join.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0049, 3 | name: "permit_join", 4 | statusExpected: true, 5 | responseExpected: false, 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.address = options.address || 0xFFFC /*broadcast*/; 11 | cmd.duration = options.duration===0 ? 0 : (parseInt(options.duration) || 254); 12 | cmd.significance = parseInt(options.significance) || 0; /* 0 = No change in authentication ; 1 = Authentication policy as spec */ 13 | 14 | cmd.payload = Buffer.alloc(4); 15 | cmd.payload.writeUInt16BE(cmd.address, 0); 16 | cmd.payload.writeUInt8(cmd.duration, 2); 17 | cmd.payload.writeUInt8(cmd.significance, 3); 18 | return cmd; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/driver/responses/status.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | // (escaped) frame exemple: 4 | // [0x01, 0x80, 0x00, 0x00, 0x05, 0xFB, 0x00, 0x3B, 0x00, 0x45, 0x00, 0x03] 5 | 6 | // parsed response example: 7 | // status(0x8000), status:{"id":0,"name":"success"}, sequence:115, relatedTo:descriptor_node(0x42), rssi:0 8 | 9 | 10 | module.exports = { 11 | id: 0x8000, 12 | name: "status", 13 | parse: function(reader, rep, version) { 14 | rep.status = Enum.STATUS(reader.nextUInt8(), (id)=>{ return {id:'0x'+id.toString(16), name:"unknown"}; }); 15 | rep.sequence = reader.nextUInt8(); 16 | rep.relatedTo = Enum.COMMANDS(reader.nextUInt16BE(), {id:0, name:'null'}); 17 | if (reader.isMore()) { 18 | rep.error = reader.restAll().toString(); 19 | } 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /devices/xiaomi.door_sensor.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.sensor_magnet','lumi.sensor_magnet.aq2']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_door_sensor', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | 16 | open: { type:'bool', attribute: {id: '0x0001 0x0006 0x0000' } }, 17 | }, 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /devices/xiaomi.remote_wall_switch_single.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.sensor_86sw1']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_remote_wall_switch_single', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | }, 16 | 17 | events: { 18 | push: { attribute: { id:'0x1 0x6 0x0000', equal:true} }, 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/driver/commands/device_remove.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0026, 3 | name: "device_remove", 4 | statusExpected: true, 5 | responseExpected: 'device_remove', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | 9 | build: function(options, cmd, version) { 10 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 11 | cmd.extended = (typeof(options.extended) === 'string' && options.extended.length === 16) ? options.extended : (()=>{throw new Error("invalid parameter 'extended'."); })(); 12 | 13 | cmd.payload = Buffer.alloc(10); 14 | 15 | cmd.payload.writeUInt16BE(cmd.address, 0) 16 | Buffer.from(cmd.extended, 'hex').copy(cmd.payload, 2); 17 | 18 | return cmd; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /devices/xiaomi.water_sensor.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.sensor_wleak.aq1']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_water_sensor', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | 16 | water: { type:'bool', attribute: {id: '0x0001 0x0500 0x0002', toValue: ((data, valueObj) => (data===1) ? true : ((data===0) ? false : undefined)) } }, 17 | }, 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /devices/xiaomi.remote_wall_switch_double.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.sensor_86sw2']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_remote_wall_switch_double', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | }, 16 | 17 | events: { 18 | push_left: { attribute: { id:'0x1 0x6 0x0000', equal:true} }, 19 | push_right: { attribute: { id:'0x2 0x6 0x0000', equal:true} }, 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/driver/commands/time_set.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | const TIME_BASE = new Date('2000-01-01 00:00:00Z').getTime(); 3 | 4 | module.exports = { 5 | id: 0x0016, 6 | name: "time_set", 7 | statusExpected: true, 8 | responseExpected: false, 9 | minVersion: 783, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 10 | 11 | build: function(options, cmd, version) { 12 | if (options.time && !(isNaN(parseInt(options.time))) ) { 13 | cmd.time = new Date(parseInt(options.time)); 14 | } 15 | else if (options.time && (options.time instanceof Date)) { 16 | cmd.time = options.time; 17 | } 18 | else if (!options.time) { 19 | cmd.time = Date.now(); 20 | } 21 | 22 | cmd.timestamp = parseInt( (cmd.time.getTime()-TIME_BASE) / 1000) ; 23 | 24 | cmd.payload = Buffer.alloc(4); 25 | cmd.payload.writeUInt32( cmd.timestamp ); 26 | return cmd; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-zigate", 3 | "version": "1.0.0", 4 | "description": "a nodejs module for zigate USB-TTL key", 5 | "main": "src/zigate.js", 6 | "dependencies": { 7 | "@serialport/parser-delimiter": "^9.0.1", 8 | "colors": "^1.3.0", 9 | "mkdirp": "^0.5.1", 10 | "serialport": "^9.0.1", 11 | "zcl-id": "^0.3.2" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "node test/test-driver.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/nouknouk/node-zigate.git" 20 | }, 21 | "keywords": [ 22 | "zigate", 23 | "zigbee", 24 | "usb" 25 | ], 26 | "author": "nouknouk@gmail.com", 27 | "license": "LGPL-3.0", 28 | "bugs": { 29 | "url": "https://github.com/nouknouk/node-zigate/issues" 30 | }, 31 | "homepage": "https://github.com/nouknouk/node-zigate#readme" 32 | } 33 | -------------------------------------------------------------------------------- /src/driver/commands/ieee_address.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0041, 3 | name: "ieee_address", 4 | statusExpected: true, 5 | responseExpected: 'ieee_address', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | build: function(options, cmd, version) { 9 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 10 | cmd.extended = !!(options.extended); 11 | cmd.start = options.start || 0; 12 | cmd.devices = options.start || 0; 13 | cmd.target = options.target || 0; 14 | 15 | cmd.payload = Buffer.alloc(6); 16 | cmd.payload.writeUInt16BE(cmd.target, 0); 17 | cmd.payload.writeUInt16BE(cmd.address, 2); 18 | cmd.payload.writeUInt8(cmd.extended, 4); // Request Type: 0 = Single ; 1 = Extended 19 | cmd.payload.writeUInt8(cmd.start, 5); 20 | return cmd; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /devices/xiaomi.vibration_sensor.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.vibration.aq1']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_vibration_sensor', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | 16 | }, 17 | 18 | events: { 19 | tilt: { attribute: { id: '0x0001 0x0101 0X0055', equal: 2 } }, 20 | catch: { attribute: { id: '0x0001 0x0101 0X0055', equal: 2 } }, 21 | fall: { attribute: { id: '0x0001 0x0101 0X0055', equal: 3 } }, 22 | }, 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /devices/xiaomi.switch_key.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.sensor_switch.aq2']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_switch_key', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | }, 16 | 17 | events: { 18 | push_x1: { attribute: { id:'0x1 0x6 0x0000', equal:true, args:[1]} }, 19 | push_x2: { attribute: { id:'0x1 0x6 0x8000', equal:2, args:[2]} }, 20 | push_x3: { attribute: { id:'0x1 0x0 0x8000', equal:3, args:[3]} }, 21 | push_x4: { attribute: { id:'0x1 0x0 0x8000', equal:4, args:[4]} }, 22 | }, 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /src/driver/responses/data_indication.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x8002, 3 | name: "data_indication", 4 | parse: function(reader, rep, version) { 5 | rep.status = Enum.COMMAND_STATUS(reader.nextUInt8(), new Error("data_indication: unknown status")); 6 | 7 | rep.profile = Enum.PROFILES(reader.nextUInt16BE(), new Error("data_indication: unknown profile")); 8 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE(), new Error("data_indication: unknown cluster")); 9 | rep.endpointSource = reader.nextUInt8(); 10 | rep.endpointDestination = reader.nextUInt8(); 11 | 12 | rep.addressSourceMode = Enum.ADDRESS_MODE(reader.nextUInt8()); 13 | rep.addressSource = rep.addressSourceMode.name === 'short' ? reader.nextUInt16BE() : reader.nextBuffer(8).toString('hex'); 14 | 15 | rep.addressDestinationMode = Enum.ADDRESS_MODE(reader.nextUInt8()); 16 | rep.addressDestination = rep.addressDestinationMode.name === 'short' ? reader.nextUInt16BE() : reader.nextBuffer(8).toString('hex'); 17 | 18 | var dataSize = reader.nextUInt8(); 19 | rep.data = []; 20 | for (var i=0; i> 4; // bit 4&5: Reserved 16 | rep.securityCapability = !!(rep.mac & 0b01000000); // bit 6: Security capacity, always 0 (standard security) 17 | rep.allocateAddress = !!(rep.mac & 0b10000000); // bit 7: 1 = joining device must be issued network address 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /devices/xiaomi.motion_sensor.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.sensor_motion','lumi.sensor_motion.aq2']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_motion_sensor', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | 16 | presence: { type:'bool', attribute: {id: '0x0001 0x0406 0x0000', toValue: ((attrval) => typeof(attrval) === 'number' ? (attrval===1) : undefined) } }, 17 | luminance: { type:'float', attribute: {id: '0x0001 0x0400 0x0000', toValue: ((attrval) => typeof(attrval) === 'number' ? (attrval/100) : undefined) }, unit: '%' , min:-0, max: 100 }, 18 | }, 19 | 20 | events: { 21 | motion: { attribute: {id: '0x0001 0x0000 0x0005', equal: 1}, }, 22 | }, 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | 61 | # zigate personal network file 62 | network_devices.json 63 | 64 | -------------------------------------------------------------------------------- /src/coordinator/ziendpoint.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const Sym = require('./symbols.js'); 3 | const ZiCluster = require('./zicluster.js'); 4 | 5 | var LOGS = { log: ()=>{}, warn: ()=>{}, error: ()=>{}, debug: ()=>{} }; 6 | 7 | class ZiEndpoint extends EventEmitter { 8 | constructor(id, device, verified) { 9 | super(); 10 | this[Sym.ID] = id; 11 | this[Sym.DEVICE] = device; 12 | this[Sym.VERIFIED] = !!verified; 13 | this[Sym.CLUSTERS] = {}; 14 | } 15 | 16 | get id() { return this[Sym.ID]; } 17 | get hex() { return "0x"+(("0000"+Number(this.id).toString(16)).substr(-4,4)); } 18 | get device() { return this[Sym.DEVICE]; } 19 | get verified() { return this[Sym.VERIFIED]; } 20 | set verified(v) { return this[Sym.VERIFIED] = v; } 21 | get clusters() { return Object.values(this[Sym.CLUSTERS]); } 22 | cluster(id) { return this[Sym.CLUSTERS][id]; } 23 | addCluster(id, verified) { return this.device[Sym.COORDINATOR].addCluster(this, id, verified); } 24 | queryClusters() { return this[Sym.COORDINATOR].queryClusters(this); } 25 | 26 | get log() { return this.device[Sym.COORDINATOR].log; } 27 | toString() { return "[endpoint_"+this.hex+"]"; } 28 | } 29 | 30 | module.exports = ZiEndpoint; 31 | -------------------------------------------------------------------------------- /devices/xiaomi.sensor_weather.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.weather']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_sensor_weather', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 15 | 16 | temperature: { type:'float', attribute: {id: '0x1 0x402 0x0', toValue: ((attrval) => typeof(attrval) === 'number' ? attrval/100 : undefined) }, unit: '°C', min:-20, max: 60 }, 17 | humidity: { type:'float', attribute: {id: '0x1 0x405 0x0', toValue: ((attrval) => typeof(attrval) === 'number' ? attrval/100 : undefined) }, unit: '%' , min:-0, max: 100 }, 18 | pressure: { type:'int', attribute: {id: '0x1 0x403 0x0' }, unit: 'hPa', min:800, max: 1200 }, 19 | }, 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /devices/xiaomi.cube_magic.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['lumi.sensor_cube']; 2 | 3 | module.exports = { 4 | 5 | id: 'xiaomi_cube_magic', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(1, 0, 5) && device.attribute(1, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0001 0x0000 0x0005' } }, 14 | rot_v: { type:'int', attribute: {id: '0x0003 0x000c 0xff05' } }, 15 | battery: { type:'float', attribute: {id: '0x0001 0x0000 0xff01', toValue: ((data, valueObj) => { return (data && data.length > 3) ? (data[3].charCodeAt(0)*256 + data[2].charCodeAt(0))/1000 : undefined; }), unit: 'V', min:0.00, max: 3.00 } }, 16 | }, 17 | 18 | events: { 19 | shake: { attribute: {id: '0x0002 0x0012 0x0055', equal: 0x0000 }, }, 20 | slide: { attribute: {id: '0x0001 0x0012 0x0055', equal: 0x0103 }, }, 21 | tap: { attribute: {id: '0x0001 0x0012 0x0055', equal: 0x0204 }, }, 22 | rotate_h: { attribute: {id: '0x0003 0x000c 0xff05', /* */}, }, // data = 0x01F4 , then 0xC2C2C2C2 ??? 23 | //rotate_v: { attribute: {id: '0x0002 0x0012 0x0055', /* equal: 0 */}, }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /test/test-driver.js: -------------------------------------------------------------------------------- 1 | 2 | // port is not mandatory: node-zigate may guess automatically (or not) the right port if none provided 3 | const port=process.argv.length >= 3 ? process.argv[2] : null; 4 | 5 | let Zigate = require(__dirname+'/..'); 6 | 7 | let myZikey = new Zigate.Driver(); 8 | 9 | myZikey.on('close', function() { 10 | console.error("connection to Zigate closed."); 11 | }); 12 | 13 | myZikey.on('error', function(err) { 14 | console.error("error: ",err); 15 | }); 16 | 17 | myZikey.on('response', function(response) { 18 | console.log("response '"+response.type.name+"' received: ", response); 19 | if (response.type.name === 'version_list') { 20 | console.log("closing connexion..."); 21 | myZikey.close().then(()=> { 22 | console.log("connexion closed. Exiting"); 23 | process.exit(0); 24 | }); 25 | } 26 | }); 27 | 28 | if (port) { 29 | console.log("opening connexion to port '"+port+"'..."); 30 | } 31 | else { 32 | console.log("No port provided ; will guess the Zigate port automatically before connecting."); 33 | } 34 | myZikey.open(port).then(()=> { 35 | console.log("connection to Zigate well established."); 36 | myZikey.send("version"); 37 | console.log("command 'version' sent ; awaiting response..."); 38 | }); 39 | -------------------------------------------------------------------------------- /src/coordinator/zicommand.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const Sym = require('./symbols.js'); 3 | const CLUSTER = Symbol("CLUSTER"); 4 | 5 | class ZiCommand extends EventEmitter { 6 | constructor(id, cluster, verified) { 7 | super(); 8 | this[Sym.ID] = id; 9 | this[Sym.CLUSTER] = cluster; 10 | this[Sym.VERIFIED] = !!verified; 11 | this[Sym.TYPE] = (cluster && cluster.type && cluster.type.commands && cluster.type.commands[id]) || null; 12 | } 13 | 14 | get id() { return this[Sym.ID]; } 15 | get hex() { return "0x"+(("0000"+Number(this.id).toString(16)).substr(-4,4)); } 16 | get type() { return this[Sym.TYPE]; } 17 | get cluster() { return this[Sym.CLUSTER]; } 18 | get endpoint() { return this[Sym.CLUSTER][Sym.ENDPOINT]; } 19 | get device() { return this[Sym.CLUSTER][Sym.ENDPOINT][Sym.DEVICE]; } 20 | get verified() { return this[Sym.VERIFIED]; } 21 | set verified(v) { return this[Sym.VERIFIED] = v; } 22 | 23 | exec() { 24 | return this.device[Sym.COORDINATOR].execCommand(this, arguments); 25 | } 26 | [Sym.ON_ACTION_EXEC]() { 27 | this.emit('action_exec', this, [...arguments]); 28 | } 29 | 30 | get log() { return this.device[Sym.COORDINATOR].log; } 31 | toString() { return "[command_"+this.type+"_"+this.hex+"]"; } 32 | } 33 | 34 | module.exports = ZiCommand; 35 | -------------------------------------------------------------------------------- /src/driver/responses/zone_status_change.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | /* 4 | 04 5 | 01 6 | 05 7 | 00 // = short (16 bits) 8 | 02 7c 9 | 97 00 10 | 01 11 | 00 12 | ff 00 00 13 | 14 | for device 0x7c97 raw data: 15 | TYPE = 84 02 16 | LENGTH = 00 0e 17 | PAYLOAD = cb 04 / 01 / 05 / 00 / 02 7c / 97 00 01 00 ff 00 00 18 | RSSI = 57 19 | */ 20 | 21 | module.exports = { 22 | id: 0x8401, 23 | name: "zone_status_change", 24 | parse: function(reader, rep, version) { 25 | rep.sequence = reader.nextUInt8(); 26 | rep.endpoint = reader.nextUInt8(); 27 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE()); 28 | rep.addressmode = Enum.ADDRESS_MODE(reader.nextUInt8()); 29 | rep.address = reader.nextUInt16BE(); 30 | rep.zonestatus = reader.nextUInt16BE(); 31 | rep.extendedstatus = reader.nextUInt8(); 32 | rep.zoneid = reader.nextUInt8(); 33 | 34 | rep.delay = []; 35 | while (reader.isMore()) { 36 | rep.delay.push(reader.nextUInt16BE()) 37 | } 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/driver/commands/action_onoff.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0092, 3 | name: "action_onoff", 4 | statusExpected: true, 5 | responseExpected: 'default_response', 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | build: function(options, cmd, version) { 9 | cmd.addressMode = Enum.ADDRESS_MODE(options.addressMode, Enum.ADDRESS_MODE('short')); 10 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 11 | cmd.endpoint = options.endpoint || (()=>{throw new Error("invalid parameter 'dstEndpoint'.")})(); 12 | cmd.endpointSource = options.endpointSource || 0x01; // the zigbee key's endpoint itself ? 13 | cmd.on = (!!options.on) || options.off === false; 14 | cmd.off = (!!options.off) || options.on === false; 15 | cmd.toggle = (!!options.toggle); 16 | 17 | cmd.payload = Buffer.alloc(6); 18 | 19 | cmd.payload.writeUInt8(cmd.addressMode.id, 0); // short address mode 20 | cmd.payload.writeUInt16BE(cmd.address, 1); 21 | cmd.payload.writeUInt8(cmd.endpointSource, 3); 22 | cmd.payload.writeUInt8(cmd.endpoint, 4); 23 | 24 | if (cmd.on) cmd.payload.writeUInt8(1, 5); 25 | else if (cmd.off) cmd.payload.writeUInt8(0, 5); 26 | else if (cmd.toggle) cmd.payload.writeUInt8(2, 5); 27 | 28 | return cmd; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/driver/responses/descriptor_simple.js: -------------------------------------------------------------------------------- 1 | // parsed response example: 2 | // descriptor_simple(0x8043), sequence:116, status:0, address:0x3dad, 3 | // length:20, endpoint:1, profile:ha(0x104), deviceType:unknown(0x5f01), 4 | // deviceVersion:1, reserved:0, inClusters:0,65535,6, outClusters:0,4,65535, rssi:108 5 | 6 | module.exports = { 7 | id: 0x8043, 8 | name: "descriptor_simple", 9 | parse: function(reader, rep, version) { 10 | rep.sequence = reader.nextUInt8(); 11 | rep.status = reader.nextUInt8(); 12 | rep.address = reader.nextUInt16BE(); 13 | rep.length = reader.nextUInt8(); // ??? 14 | rep.endpoint = reader.nextUInt8(); 15 | rep.profile = Enum.PROFILES(reader.nextUInt16BE()); 16 | 17 | var haType = reader.nextUInt16BE(); 18 | rep.deviceType = Enum.DEVICE_HA_TYPE(haType, { id:haType, name:'unknown_0x'+haType.toString(16) });; 19 | 20 | var bitsField = reader.nextUInt8(); 21 | rep.deviceVersion = bitsField & 0x0F; // bits 0-4 22 | rep.reserved = bitsField >> 4; // bits 4-7 23 | 24 | rep.inClusters = []; 25 | var inCount = reader.nextUInt8(); 26 | for(let i=0; i{throw new Error("invalid parameter 'address'.");})(); 11 | cmd.endpoint = options.endpoint || (()=>{throw new Error("invalid parameter 'dstEndpoint'.")})(); 12 | cmd.endpointSource = options.endpointSource || 0x01; // the zigbee key's endpoint itself ? 13 | cmd.on = (!!options.on) || options.off === false; 14 | cmd.off = (!!options.off) || options.on === false; 15 | cmd.ontime = (!!options.on) || 0; 16 | cmd.offtime = (!!options.on) || 0; 17 | 18 | cmd.payload = Buffer.alloc(6); 19 | 20 | cmd.payload.writeUInt8(cmd.addressMode.id, 0); // short address mode 21 | cmd.payload.writeUInt16BE(cmd.address, 1); 22 | cmd.payload.writeUInt8(cmd.endpointSource, 3); 23 | cmd.payload.writeUInt8(cmd.endpoint, 4); 24 | 25 | if (command.on) { 26 | cmd.payload.writeUInt8(1, 5); 27 | cmd.payload.writeUint8(cmd.ontime, 6); 28 | cmd.payload.writeUint8(cmd.offtime, 8); 29 | } 30 | if (command.off) { 31 | cmd.payload.writeUInt8(0, 5); 32 | cmd.payload.writeUint8(cmd.ontime, 6); 33 | cmd.payload.writeUint8(cmd.offtime, 8); 34 | } 35 | 36 | return cmd; 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/coordinator/zicluster.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const Sym = require('./symbols.js'); 3 | let Enum = require('../driver/enum.js'); 4 | let ZiAttribute = require('./ziattribute.js'); 5 | let ZiCommand = require('./zicommand.js'); 6 | 7 | class ZiCluster extends EventEmitter { 8 | constructor(id, endpoint, verified) { 9 | super(); 10 | this[Sym.ID] = id; 11 | this[Sym.ENDPOINT] = endpoint; 12 | this[Sym.TYPE] = Enum.CLUSTERS(id); 13 | this[Sym.VERIFIED] = !!verified; 14 | this[Sym.ATTRIBUTES] = {}; 15 | this[Sym.COMMANDS] = {}; 16 | } 17 | 18 | get id() { return this[Sym.ID]; } 19 | get hex() { return "0x"+(("0000"+Number(this.id).toString(16)).substr(-4,4)); } 20 | get type() { return this[Sym.TYPE]; } 21 | get verified() { return this[Sym.VERIFIED]; } 22 | set verified(v) { return this[Sym.VERIFIED] = v; } 23 | get endpoint() { return this[Sym.ENDPOINT]; } 24 | get device() { return this[Sym.ENDPOINT][Sym.DEVICE]; } 25 | 26 | get attributes() { return Object.values(this[Sym.ATTRIBUTES]); } 27 | attribute(id) { return this[Sym.ATTRIBUTES][id]; } 28 | addAttribute(id, value, verified) { return this.device[Sym.COORDINATOR].addAttribute(this, id, value, verified); } 29 | queryAttributes() { return this[Sym.COORDINATOR].queryAttributes(this); } 30 | 31 | get commands() { return Object.values(this[Sym.COMMANDS]); } 32 | command(id) { return this[Sym.COMMANDS][id]; } 33 | addCommand(id, verified) { return this.device[Sym.COORDINATOR].addCommand(this, id); } 34 | 35 | get log() { return this.device[Sym.COORDINATOR].log; } 36 | toString() { return "[cluster_"+this.type+"]"; } 37 | } 38 | 39 | module.exports = ZiCluster; 40 | -------------------------------------------------------------------------------- /src/driver/commands/attribute_discovery.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x0140, 5 | name: "attribute_discovery", 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | build: function(options, cmd, version) { 9 | cmd.addressMode = Enum.ADDRESS_MODE(options.addressMode, Enum.ADDRESS_MODE('short')); 10 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 11 | cmd.endpoint = options.endpoint || (()=>{throw new Error("invalid parameter 'endpoint'.")})(); 12 | cmd.endpointSource = options.endpointSource || 0x01; // the zigbee key's endpoint itself ? 13 | cmd.cluster = Enum.CLUSTERS(options.cluster, {id: options.cluster, name:'unknown_0x'+options.cluster.toString(16) }); 14 | cmd.firstId = options.firstId || 0x0000; 15 | cmd.direction = Enum.DIRECTION(options.direction, Enum.DIRECTION('srv_to_cli')); 16 | cmd.manufacturer = cmd.manufacturer || false; 17 | cmd.count = options.count || 0x5; 18 | 19 | cmd.payload = Buffer.alloc(14); 20 | 21 | cmd.payload.writeUInt8(cmd.addressMode.id, 0); // short address mode 22 | cmd.payload.writeUInt16BE(cmd.address, 1); 23 | cmd.payload.writeUInt8(cmd.endpointSource, 3); 24 | cmd.payload.writeUInt8(cmd.endpoint, 4); 25 | cmd.payload.writeUInt16BE(cmd.cluster.id, 5); 26 | cmd.payload.writeUInt16BE(cmd.firstId, 7); 27 | cmd.payload.writeUInt8(cmd.direction.id, 9); 28 | cmd.payload.writeUInt8((cmd.manufacturer ? 1 : 0), 10); /* manufacturer specific */ 29 | cmd.payload.writeUInt16BE((cmd.manufacturer || 0), 11); 30 | cmd.payload.writeUInt8(cmd.count, 13); 31 | 32 | return cmd; 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/driver/clusters/genGroups.js: -------------------------------------------------------------------------------- 1 | 2 | nameSupportBits= { 3 | 0: 'supported', 4 | }; 5 | 6 | 7 | module.exports = { 8 | "id": 4, 9 | "name": "genGroups", 10 | "specific": null, 11 | "attributes": { 12 | "0": { "cluster": 4, "id": 0, "name": "nameSupport", "type": "bitmap8", "mandatory": true, "read": true, "write": false, "specific": false, bits: nameSupportBits } 13 | }, 14 | "commands": { 15 | "0": { "id": 0, "name": "add", mandatory: true, params:[{name:'id', type:'uint16'}, {name:'name', type:'string'}] }, 16 | "1": { "id": 1, "name": "view", mandatory: true, params:[{name:'id', type:'uint16'}] }, 17 | "2": { "id": 2, "name": "getMembership", mandatory: true, params:[{name:'count', type:'uint8'}, {name:'list', type:'list'}] }, 18 | "3": { "id": 3, "name": "remove", mandatory: true, params:[{name:'id', type:'uint16'}] }, 19 | "4": { "id": 4, "name": "removeAll", mandatory: true, params:[] }, 20 | "5": { "id": 5, "name": "addIfIdentifying", mandatory: true, params:[{name:'id', type:'uint16'}, {name:'name', type:'string'}] } 21 | }, 22 | "responses": { 23 | "0": { "id": 0, "name": "addRsp", mandatory: true, params:[{name:'status', type:'enum8'}, {name:'id', type:'uint16'}] }, 24 | "1": { "id": 1, "name": "viewRsp", mandatory: true, params:[{name:'status', type:'enum8'}, {name:'id', type:'uint16'}, {name:'name', type:'string'}] }, 25 | "2": { "id": 2, "name": "getMembershipRsp", mandatory: true, params:[{name:'capacity', type:'uint8'}, {name:'count', type:'uint8'}, {name:'list', type:'list'}]}, 26 | "3": { "id": 3, "name": "removeRsp", mandatory: true, params:[{name:'status', type:'enum8'}, {name:'id', type:'uint16'}] } 27 | }, 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /src/driver/responses/attribute_write.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | // (escaped) frame exemple: 4 | // Buffer.from([0x01, 0x81, 0x02, 0x12, 0x02, 0x10, 0x19, 0x7F, 0x02, 0x10, 0xE1, 0xE1, 0x02, 0x11, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x15, 0x02, 0x10, 0x42, 0x02, 0x10, 0x02, 0x1C, 0x6C, 0x75, 0x6D, 0x69, 0x2E, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0xE4, 0x03]) 5 | 6 | module.exports = { 7 | id: 0x8110, 8 | name: "attribute_write", 9 | parse: function(reader, rep, version) { 10 | rep.sequence = reader.nextUInt8(); 11 | rep.address = reader.nextUInt16BE(); 12 | rep.endpoint = reader.nextUInt8(); 13 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE()); 14 | rep.id = reader.nextUInt16BE(); 15 | rep.status = Enum.ATTRIBUTE_STATUS(reader.nextUInt8()); 16 | rep.type = Enum.ATTRIBUTE_TYPE(reader.nextUInt8(), new Error('unknown attribute type ')); 17 | rep.value = undefined; 18 | 19 | var attributeSize = reader.nextUInt16BE(); 20 | var valueData = reader.nextBuffer(attributeSize); 21 | switch (rep.type.id) { 22 | case 0x00: // null 23 | rep.value = null; 24 | break; 25 | case 0x10: // boolean 26 | rep.value = valueData.readUIntBE(valueData.length) ? true : false; 27 | break; 28 | case 0x18: // bitmap8 29 | rep.value = valueData.readUIntBE(valueData.length); 30 | break; 31 | case 0x20: // uint8 32 | case 0x21: // uint16 33 | case 0x22: // uint32 34 | case 0x25: // uint48 35 | rep.value = valueData.readUIntBE(valueData.length); 36 | break; 37 | case 0x28: // int8 38 | case 0x29: // int16 39 | case 0x2a: // int32 40 | rep.value = valueData.readIntBE(valueData.length); 41 | break; 42 | case 0x30: // enum 43 | rep.value = valueData.readUIntBE(valueData.length); 44 | break; 45 | case 0x42: // string 46 | rep.value = valueData.toString(); 47 | break; 48 | } 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/driver/clusters/genDeviceTempCfg.js: -------------------------------------------------------------------------------- 1 | tempAlarmBits = { 2 | 0: 'temperature too low', 3 | 1:'temperature too high', 4 | }; 5 | 6 | 7 | module.exports = { 8 | "id": 2, 9 | "name": "genDeviceTempCfg", 10 | "specific": null, 11 | "attributes": { 12 | "0": { "cluster": 2, "id": 0, "name": "currentTemperature", "type": "int16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '°C' }, 13 | "1": { "cluster": 2, "id": 1, "name": "minTempExperienced", "type": "int16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '°C' }, 14 | "2": { "cluster": 2, "id": 2, "name": "maxTempExperienced", "type": "int16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '°C' }, 15 | "3": { "cluster": 2, "id": 3, "name": "overTempTotalDwell", "type": "uint16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": 'hour' }, 16 | "16": { "cluster": 2, "id": 16, "name": "devTempAlarmMask", "type": "bitmap8", "mandatory": false, "read": true, "write": true, "specific": false, "unit": 'tempAlarmBits' }, 17 | "17": { "cluster": 2, "id": 17, "name": "lowTempThres", "type": "int16", "mandatory": false, "read": true, "write": true, "specific": false, "unit": '°C' }, 18 | "18": { "cluster": 2, "id": 18, "name": "highTempThres", "type": "int16", "mandatory": false, "read": true, "write": true, "specific": false, "unit": '°C' }, 19 | "19": { "cluster": 2, "id": 19, "name": "lowTempDwellTripPoint", "type": "uint24", "mandatory": false, "read": true, "write": true, "specific": false, "unit": 'second' }, 20 | "20": { "cluster": 2, "id": 20, "name": "highTempDwellTripPoint", "type": "uint24", "mandatory": false, "read": true, "write": true, "specific": false, "unit": 'second' } 21 | }, 22 | "commands": { 23 | }, 24 | "responses": { 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /doc/compaptible device infos.txt: -------------------------------------------------------------------------------- 1 | "zcl-id": "^0.3.2", 2 | "zcl-packet": "^0.2.0", 3 | "ziee": "^0.3.0", 4 | "zive": "^0.2.2", 5 | "zstack-constants": "^0.2.0" 6 | 7 | 8 | 9 | Xiaomi Mi (new version) temperature sensor Xiaomi mi aquara 10 | 11 | EndPoint: 0x01 12 | Profile ID: 0x0104 (ZigBee HA) 13 | Device ID: 0x5F01 (Unknown) 14 | Input Cluster Count: 6 15 | Cluster 0: Cluster ID: 0x0000 (General: Basic) 16 | Cluster 1: Cluster ID: 0x0003 (General: Identify) 17 | Cluster 2: Cluster ID: 0xFFFF (Unknown) 18 | Cluster 3: Cluster ID: 0x0402 (Measurement: Temperature) 19 | Cluster 4: Cluster ID: 0x0403 (Unknown) 20 | Cluster 5: Cluster ID: 0x0405 (Unknown) 21 | Output Cluster Count: 3 22 | Cluster 0: Cluster ID: 0x0000 (General: Basic) 23 | Cluster 1: Cluster ID: 0x0003 (General: Identify) 24 | Cluster 2: Cluster ID: 0xFFFF (Unknown) 25 | 26 | Attributes: 27 | Temperature 28 | Cluster ID: 0x0402 (Measurement: Temperature) 29 | Attribute ID: 0x0000 30 | Attribute Type: 0x29 (INT16) 31 | data/100 = 22.98°C 32 | Humidity 33 | Cluster ID: 0x0405 (Humidity) 34 | Attribute ID: 0x0000 35 | Attribute Type: 0x21 (UINT16) 36 | data/100 = 75.99% 37 | Pressure 38 | Cluster ID: 0x0403 (Pression atmosphérique) 39 | Attribute ID: 0x0000 40 | Attribute Type: 0x29 (INT16) 41 | data = 1009mb 42 | 43 | Cluster ID: 0x0403 (Pression atmosphérique) 44 | Attribute ID: 0x0014 45 | Attribute Type: 0x28 (Unknown) 46 | data = unknown 47 | 48 | Cluster ID: 0x0403 (Pression atmosphérique) 49 | Attribute ID: 0x0010 50 | Attribute Type: 0x29 (INT16) 51 | data / 10 = 1009.8mb 52 | 53 | Battery level 54 | Cluster ID: 0x0000 (General: Basic) 55 | Attribute ID: 0xFF01 56 | Attribute Size: 0x0025 57 | Attribute Type: 0x42 (Character String) 58 | 59 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 60 | 61 | Xiaomi Mi (new version) -------------------------------------------------------------------------------- /src/driver/commands/attribute_read.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | module.exports = { 4 | id: 0x0100, 5 | name: "attribute_read", 6 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 7 | 8 | build: function(options, cmd, version) { 9 | cmd.addressMode = Enum.ADDRESS_MODE(options.addressMode, Enum.ADDRESS_MODE('short')); 10 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 11 | cmd.endpoint = !(isNaN(parseInt(options.endpoint))) ? parseInt(options.endpoint) : (()=>{throw new Error("invalid parameter 'endpoint'.")})(); 12 | cmd.endpointSource = !(isNaN(parseInt(options.endpointSource))) ? options.endpointSource : 0x01; // the zigbee key's endpoint itself ? 13 | cmd.cluster = Enum.CLUSTERS(options.cluster, new Error("invalid parameter 'cluster'.")); 14 | cmd.direction = Enum.DIRECTION(options.direction, Enum.DIRECTION('srv_to_cli')); 15 | cmd.manufacturer = cmd.manufacturer || false; 16 | cmd.attributes = options.attributes || (()=>{throw new Error("invalid parameter 'attributes'.");})(); 17 | 18 | cmd.payload = Buffer.alloc(12+2*cmd.attributes.length); 19 | 20 | cmd.payload.writeUInt8(cmd.addressMode.id, 0); // short address mode 21 | cmd.payload.writeUInt16BE(cmd.address, 1); 22 | cmd.payload.writeUInt8(cmd.endpointSource, 3); 23 | cmd.payload.writeUInt8(cmd.endpoint, 4); 24 | cmd.payload.writeUInt16BE(cmd.cluster.id, 5); 25 | cmd.payload.writeUInt8(cmd.direction.id, 7); 26 | cmd.payload.writeUInt8( (cmd.manufacturer ? 1 : 0), 8); /* manufacturer specific */ 27 | cmd.payload.writeUInt16BE( (cmd.manufacturer || 0), 9); 28 | 29 | cmd.payload.writeUInt8(cmd.attributes.length || 0, 11); 30 | for (let i=0; i { return action_exec.apply(this, args); }); 58 | } 59 | else { 60 | ret = Promise.reject('no action defined'); 61 | } 62 | this.emit('action_exec', this, args, ret); 63 | this.log.debug(''+this.device+''+this+' action executed.'); 64 | return ret; 65 | } 66 | 67 | toString() { return "[action_"+this.id+"]"; } 68 | [util.inspect.custom](depth, opts) { return ""+this+" ("+JSON.stringify(this.definition)+")"; } 69 | } 70 | 71 | module.exports = Action; 72 | -------------------------------------------------------------------------------- /src/coordinator/symbols.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | LOG: Symbol("LOG"), 3 | STATUS: Symbol("STATUS"), 4 | VERSION_MAJOR: Symbol("VERSION_MAJOR"), 5 | VERSION_INSTALLER: Symbol("VERSION_INSTALLER"), 6 | ADDRESS: Symbol("ADDRESS"), 7 | IEEE: Symbol("IEEE"), 8 | ID: Symbol("ID"), 9 | TYPE: Symbol("TYPE"), 10 | DEVICE: Symbol("DEVICE"), 11 | DEVICES: Symbol("DEVICES"), 12 | PROFILE: Symbol("PROFILE"), 13 | READY: Symbol("READY"), 14 | ENDPOINT: Symbol("ENDPOINT"), 15 | ENDPOINTS: Symbol("ENDPOINTS"), 16 | CLUSTER: Symbol("CLUSTER"), 17 | CLUSTERS: Symbol("CLUSTERS"), 18 | ATTRIBUTE: Symbol("ATTRIBUTE"), 19 | ATTRIBUTES: Symbol("ATTRIBUTES"), 20 | ATTR_DATA: Symbol("ATTR_DATA"), 21 | COMMAND: Symbol("COMMAND"), 22 | COMMANDS: Symbol("COMMANDS"), 23 | ACTION: Symbol("ACTION"), 24 | ACTIONS: Symbol("ACTIONS"), 25 | VALUE: Symbol("VALUE"), 26 | VALUES: Symbol("VALUES"), 27 | DEFINITION: Symbol("DEFINITION"), 28 | VALUE_DATA: Symbol("VALUE_DATA"), 29 | SET_VALUE_DATA: Symbol("SET_VALUE_DATA"), 30 | VALUE_CB: Symbol("VALUE_CB"), 31 | VALUE_BOUND_ATTR: Symbol("VALUE_BOUND_ATTR"), 32 | EVENTS: Symbol("EVENTS"), 33 | VERIFIED: Symbol("VERIFIED"), 34 | BATTERY: Symbol("BATTERY"), 35 | ON_BATTERY_CHANGE: Symbol("ON_BATTERY_CHANGE"), 36 | 37 | // functions & data in mappings 38 | SETUP: Symbol("SETUP"), 39 | DESTROY: Symbol("DESTROY"), 40 | SET_ATTR_DATA: Symbol("SET_ATTR_DATA"), 41 | SET_VALUE: Symbol("SET_VALUE"), 42 | 43 | ACTION_EXEC: Symbol("ACTION_EXEC"), 44 | ACTION_DEF: Symbol("ACTION_DEF"), 45 | 46 | EVENT_FIRE: Symbol("EVENT_FIRE"), 47 | EVENT_DEF: Symbol("EVENT_DEF"), 48 | EVENT_CB: Symbol("EVENT_CB"), 49 | 50 | 51 | // device callbacks calld by coordinator. 52 | ON_TYPE_CHANGE: Symbol("ON_TYPE_CHANGE"), 53 | ON_ENDPOINT_ADD: Symbol("ON_ENDPOINT_ADD"), 54 | ON_CLUSTER_ADD: Symbol("ON_CLUSTER_ADD"), 55 | ON_ATTRIBUTE_ADD: Symbol("ON_ATTRIBUTE_ADD"), 56 | ON_ATTRIBUTE_CHANGE: Symbol("ON_ATTRIBUTE_CHANGE"), 57 | ON_COMMAND_ADD: Symbol("ON_COMMAND_ADD"), 58 | ON_VALUE_ADD: Symbol("ON_VALUE_ADD"), 59 | ON_VALUE_REMOVE: Symbol("ON_VALUE_REMOVE"), 60 | ON_VALUE_CHANGE: Symbol("ON_VALUE_CHANGE"), 61 | ON_ACTION_ADD: Symbol("ON_ACTION_ADD"), 62 | ON_ACTION_REMOVE: Symbol("ON_ACTION_REMOVE"), 63 | ON_ACTION_EXEC: Symbol("ON_ACTION_EXEC"), 64 | ON_EVENT_ADD: Symbol("ON_EVENT_ADD"), 65 | ON_EVENT_REMOVE: Symbol("ON_EVENT_REMOVE"), 66 | ON_EVENT_FIRE: Symbol("ON_EVENT_FIRE"), 67 | }; 68 | -------------------------------------------------------------------------------- /src/driver/responses/descriptor_node.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | // (escaped) frame exemple: 4 | // [0x01, 0x81, 0x02, 0x12, 0x02, 0x10, 0x19, 0x7F, 0x02, 0x10, 0xE1, 0xE1, 0x02, 5 | // 0x11, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x15, 0x02, 0x10, 0x42, 0x02, 6 | // 0x10, 0x02, 0x1C, 0x6C, 0x75, 0x6D, 0x69, 0x2E, 0x77, 0x65, 0x61, 0x74, 0x68, 7 | // 0x65, 0x72, 0xE4, 0x03] 8 | 9 | // parsed response example: 10 | // descriptor_node(0x8042), sequence:115, address:0x3d, manufacturer:44304, 11 | // maxRxSize:14080, maxTxSize:25600, serverFlags:[object Object], 12 | // alternatePanCoordinator:false, deviceType:false, powerSource:false, 13 | // receiverOnWhenIdle:false, securityCapability:false, allocateAddress:false, 14 | // maxBufferSize:128, nodeType:undefined, 15 | // complexDescriptorAvailable:true, userDescriptorAvailable:true, 16 | // reserved:7, apsFlags:2, frequencyBand:0, rssi:108 17 | 18 | module.exports = { 19 | id: 0x8042, 20 | name: "descriptor_node", 21 | parse: function(reader, rep, version) { 22 | rep.sequence = reader.nextUInt8(); 23 | rep.address = reader.nextUInt16BE(); 24 | rep.manufacturer = reader.nextUInt16BE(); 25 | rep.maxRxSize = reader.nextUInt16BE(); 26 | rep.maxTxSize = reader.nextUInt16BE(); 27 | 28 | var serverMask = reader.nextUInt16BE(); 29 | rep.serverFlags = { 30 | primaryTrustCenter: !!((serverMask >> 15) & 0x1), 31 | backupTrustCenter: !!((serverMask >> 14) & 0x1), 32 | primaryBindingCache: !!((serverMask >> 13) & 0x1), 33 | backupBindingCache: !!((serverMask >> 12) & 0x1), 34 | primaryDiscoveryCache: !!((serverMask >> 11) & 0x1), 35 | backupDiscoveryCache: !!((serverMask >> 10) & 0x1), 36 | networkManager: !!((serverMask >> 9) & 0x1), 37 | }; 38 | 39 | var descriptorCapability = reader.nextUInt8(); 40 | 41 | var macFlags = reader.nextUInt8(); 42 | rep.alternatePanCoordinator = !!((macFlags >> 7) & 0x1); // is node able to act as co-ordinator 43 | rep.fullFunctionDevice = !!((macFlags >> 6) & 0x1); // true = full function device ; false = reduced 44 | rep.ACpowerCource = !!((macFlags >> 5) & 0x1); // true = AC powered ; false = battery 45 | rep.receiverOnWhenIdle = !!((macFlags >> 4) & 0x1); 46 | rep.highsecurityCapability = !!((macFlags >> 1) & 0x1); // true = high security 47 | rep.allocateAddress = !!( macFlags & 0x1); // should address be allocated to node 48 | 49 | rep.maxBufferSize = reader.nextUInt8(); 50 | 51 | var bitFields = reader.nextUInt16BE(); 52 | rep.nodeType = Enum.NODE_LOGICAL_TYPE(bitFields >> 13); 53 | rep.complexDescriptorAvailable = !!( (bitFields>>12) & 0x1); 54 | rep.userDescriptorAvailable = !!( (bitFields>>11) & 0x1); 55 | rep.reserved = (bitFields>>8) & 0x7; 56 | rep.apsFlags = (bitFields>>5) & 0x7; 57 | rep.frequencyBand = bitFields & 0x7; 58 | 59 | 60 | 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/coordinator/event.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const EventEmitter = require('events').EventEmitter; 3 | const Sym = require('./symbols.js'); 4 | 5 | class Event extends EventEmitter { 6 | constructor(id, device, eventdef) { 7 | super(); 8 | 9 | if (!id) throw new Error("no id provided"); 10 | if (!device) throw new Error("no device provided"); 11 | if (!eventdef) throw new Error("no definition provided"); 12 | 13 | this[Sym.ID] = id; 14 | this[Sym.DEVICE] = device; 15 | this[Sym.EVENT_DEF] = eventdef; 16 | this[Sym.EVENT_CB] = []; 17 | } 18 | 19 | get id() { return this[Sym.ID]; } 20 | get definition() { return this[Sym.event_DEF]; } 21 | get device() { return this[Sym.DEVICE]; } 22 | fire() { return this[Sym.DEVICE][Sym.COORDINATOR].fireEvent(this, arguments); } 23 | 24 | [Sym.SETUP]() { 25 | let def = this[Sym.EVENT_DEF]; 26 | this.log.debug('setup of event '+this+' with ('+JSON.stringify(def)+')...'); 27 | 28 | // setup command binding 29 | if (def.attribute) { 30 | let attributeaddr = /^\W*((?:0x)?[0-9a-fA-F]+)\W+((?:0x)?[0-9a-fA-F]+)\W+((?:0x)?[0-9a-fA-F]+)\W*$/gi.exec(def.attribute.id || ""+def.attribute) 31 | if (!attributeaddr) throw new Error("invalid attribue for event "+this.id+": attribute id = '"+(def.attribute.id || ""+def.attribute)+"'"); 32 | 33 | let endpointId = parseInt(attributeaddr[1]); 34 | let clusterId = parseInt(attributeaddr[2]); 35 | let attributeId = parseInt(attributeaddr[3]); 36 | this[Sym.EVENT_CB].push(['attribute_change', (attr, newval, oldval) => { 37 | // skip un-relevant cases 38 | if (attr.id !== attributeId || attr.cluster.id !== clusterId || attr.endpoint.id !== endpointId) return; 39 | if (def.attribute.equal && newval !== def.attribute.equal) return; 40 | if (def.attribute.notequal && newval === def.attribute.notequal) return; 41 | if (def.attribute.was && oldval !== def.attribute.was) return; 42 | if (def.attribute.wasnot && oldval === def.attribute.wasnot) return; 43 | if (def.attribute.changeonly && newval === oldval) return; 44 | 45 | let args = [newval]; 46 | if (typeof(def.attribute.args) === 'function') args = def.attribute.args.apply(this, [newval, oldval]); 47 | else if (typeof(def.attribute.args) !== undefined) args = def.attribute.args; 48 | this[Sym.DEVICE][Sym.COORDINATOR].fireEvent(this, args) 49 | }]); 50 | } 51 | this[Sym.EVENT_CB].forEach( cb => this.device.on(cb[0], cb[1]) ); 52 | } 53 | 54 | [Sym.EVENT_FIRE](args) { 55 | this.log.debug(''+this.device+''+this+' event fired.'); 56 | this.emit(this.id, args); 57 | 58 | } 59 | 60 | [Sym.DESTROY]() { 61 | this[Sym.EVENT_CB].forEach( cb => this.device.removeListener(cb[0], cb[1]) ); 62 | } 63 | 64 | get log() { return this.device.log; } 65 | toString() { return "[event_"+this.id+"]"; } 66 | 67 | [util.inspect.custom](depth, opts) { return ""+this+" ("+JSON.stringify(this.definition)+")"; } 68 | } 69 | 70 | module.exports = Event; 71 | -------------------------------------------------------------------------------- /doc/device inclusion - messages workflow.txt: -------------------------------------------------------------------------------- 1 | [OUT] active_endpoint_request(undefined) {"target":30682,"name":"active_endpoint_request"} 2 | [IN] status(8000) {"type":{"id":32768,"name":"status","typeHex":"8000"},"status":0,"srcSequence":162,"satusText":"success","requestType":69} 3 | [IN] active_endpoint_response(8045) {"type":{"id":32837,"name":"active_endpoint_response","typeHex":"8045"},"sequence":162,"status":0,"srcAddress":30682,"endpoints":[1],"rssi":198} 4 | 5 | [device_0x77da] endpoints retrieved ; gathering clusters... 6 | 7 | [OUT] simple_descriptor_request(undefined) {"target":30682,"endpoint":1,"name":"simple_descriptor_request"} 8 | [IN] status(8000) {"type":{"id":32768,"name":"status","typeHex":"8000"},"status":0,"srcSequence":163,"satusText":"success","requestType":67} 9 | [ResponseBuilder_32835] the 6 last bytes of data has not been readen: 10 | [ResponseBuilder_32835] response payload: a3 00 77 da 1a 01 01 04 5f 01 01 06 00 00 00 03 ff ff 04 02 04 03 04 05 03 00 00 00 03 ff ff 04 02 04 03 04 05 11 | 12 | [IN] simple_descriptor_response(8043) {"type":{"id":32835,"name":"simple_descriptor_response","typeHex":"8043"},"srcSequence":163,"status":0,"srcAddress":30682,"length":26,"endpoint":1,"profileId":260,"deviceId":24321,"deviceVersion":1,"reserved":0,"inClusters":[0,3,65535,1026,1027,1029],"outClusters":[0,3,65535],"rssi":198} 13 | [device_0x77da][endpoint_1] clusters retrieved ; gathering attributes... 14 | 15 | 16 | -- xiaomi magic controller 17 | 18 | permit_join(0x49), address:0xfffc, duration:60, significance:0, timestamp:2018-7-7 10:33:52.851 19 | status(0x8000), status:{"id":0,"name":"success"}, sequence:116, relatedTo:permit_join(0x49), rssi:0 20 | 21 | 22 | device_announce(0x4d), address:0xf34e, ieee:00158d000101be09, alternatePanCoordinator:false, fullFunctionDevice:false, mainsPowerSource:false, receiverOnWhenIdle:false, securityCapability:false, allocateAddress:false, rssi:219 23 | 24 | device_announce(0x4d), address:0xf34e, ieee:00158d000101be09, alternatePanCoordinator:false, fullFunctionDevice:false, mainsPowerSource:false, receiverOnWhenIdle:false, securityCapability:false, allocateAddress:false, rssi:201 25 | ===> [device_0xf34e]: device_announce received but skipped as this device is already registered. 26 | 27 | 28 | router_discovery(0x8701), status:0, networkStatus:0, rssi:201 29 | 30 | attribute_report(0x8102), sequence:0, address:0xf34e, endpoint:1, cluster:genBasic(0x0), attribute:5, definition:modelId, status:{"id":0,"name":"success","description":"Command was successful"}, valuetype:string(0x42), value:"lumi.sensor_cube", rssi:201 31 | attribute_report(0x8102), sequence:0, address:0xf34e, endpoint:1, cluster:genBasic(0x0), attribute:1, definition:appVersion, status:{"id":0,"name":"success","description":"Command was successful"}, valuetype:uint8(0x20), value:3, rssi:201 32 | attribute_report(0x8102), sequence:1, address:0xf34e, endpoint:1, cluster:genBasic(0x0), attribute:65281, definition:xiaomiCustom1, status:{"id":0,"name":"success","description":"Command was successful"}, valuetype:string(0x42), value:"\u0001!�\u000b\u0003(\u0019\u0004!�\u0001\u0005!�\u0000\u0006$\u0001\u0000\u0000\u0000\u0000\n!\u0000\u0000�!\u0000\u0000�!\u0000\u0000�!\u0000\u0000�!\u0003\u0000", rssi:201 33 | attribute_report(0x8102), sequence:2, address:0xf34e, endpoint:2, cluster:genMultistateInput(0x12), attribute:85, definition:presentValue, status:{"id":0,"name":"success","description":"Command was successful"}, valuetype:uint16(0x21), value:107, rssi:174 34 | -------------------------------------------------------------------------------- /src/driver/commands/attribute_write.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: 0x0110, 3 | name: "attribute_write", 4 | minVersion: 0, // 3.0a = 778 ; 3.0d = 781 ; 3.0f = 783 ; 3.1a = 794 5 | 6 | 7 | build: function(options, cmd, version) { 8 | cmd.addressMode = Enum.ADDRESS_MODE(options.addressMode, Enum.ADDRESS_MODE('short')); 9 | cmd.address = !(isNaN(parseInt(options.address))) ? parseInt(options.address) : (()=>{throw new Error("invalid parameter 'address'.");})(); 10 | cmd.endpoint = options.endpoint || (()=>{throw new Error("invalid parameter 'dstEndpoint'.")})(); 11 | cmd.endpointSource = options.endpointSource || 0x01; // the zigbee key's endpoint itself ? 12 | cmd.cluster = Enum.CLUSTERS(options.cluster, new Error("invalid parameter 'cluster'.")); 13 | cmd.direction = Enum.DIRECTION(options.direction, Enum.DIRECTION('cli_to_srv')); 14 | cmd.manufacturer = cmd.manufacturer || false; 15 | cmd.attribute = cmd.cluster.attributes[options.attribute] || (()=>{throw new Error("invalid parameter 'attribute'.");})(); 16 | cmd.value = typeof(options.value) !== 'undefined' ? options.value : (()=>{throw new Error("invalid parameter 'value'.");})(); 17 | 18 | var type = Enum.ATTRIBUTE_TYPE(cmd.attribute.type, new Error("unknown attribute type '"+cmd.attribute.type+"'")); 19 | var bufvalue = null; 20 | switch (type.id) { 21 | case 0x00: // null 22 | bufvalue = Buffer.alloc(0); 23 | break; 24 | case 0x10: // boolean 25 | bufvalue = Buffer.alloc(1); 26 | bufvalue.writeUInt8( (cmd.value ? 1 : 0), 0); 27 | break; 28 | case 0x18: // bitmap8 29 | bufvalue = Buffer.alloc(1); 30 | bufvalue.writeUInt8(cmd.value, 0); 31 | break; 32 | case 0x20: // uint8 33 | bufvalue = Buffer.alloc(1); 34 | bufvalue.writeUInt8(cmd.value, 0); 35 | break; 36 | case 0x21: // uint16 37 | bufvalue = Buffer.alloc(2); 38 | bufvalue.writeUInt16BE(cmd.value, 0); 39 | break; 40 | case 0x22: // uint32 41 | bufvalue = Buffer.alloc(4); 42 | bufvalue.writeUInt32BE(cmd.value, 0); 43 | break; 44 | case 0x25: // uint48 45 | break; 46 | case 0x28: // int8 47 | bufvalue = Buffer.alloc(1); 48 | bufvalue.writeInt8(cmd.value, 0); 49 | break; 50 | case 0x29: // int16 51 | bufvalue = Buffer.alloc(2); 52 | bufvalue.writeInt16BE(cmd.value, 0); 53 | break; 54 | case 0x2a: // int32 55 | bufvalue = Buffer.alloc(4); 56 | bufvalue.writeInt32BE(cmd.value, 0); 57 | break; 58 | case 0x30: // enum8 59 | bufvalue = Buffer.alloc(1); 60 | bufvalue.writeUInt8(cmd.value, 0); 61 | break; 62 | case 0x31: // enum16 63 | bufvalue = Buffer.alloc(2); 64 | bufvalue.writeUInt16BE(cmd.value, 0); 65 | break; 66 | case 0x42: // string 67 | bufvalue = Buffer.alloc(cmd.value.length+1); 68 | bufvalue.writeUInt8(cmd.value.length,0); 69 | for (var i=0; i { 31 | var cmdPath = path.resolve(cmdDir + "/" + id); 32 | try { 33 | var cmd = require(cmdPath); 34 | } catch (e) { 35 | console.error("exception while loading command '" + id + "'."); 36 | throw e; 37 | } 38 | Enum.COMMANDS.add(cmd); 39 | }); 40 | } 41 | 42 | build(typeOrOptions, options) { 43 | var type = (typeof(typeOrOptions) === 'object') ? typeOrOptions.type : ""+typeOrOptions; 44 | var options = ((typeof(typeOrOptions) === 'object') ? typeOrOptions : options) || {}; 45 | 46 | var commandType = Enum.COMMANDS(type, new Error("invalid command type name '"+type+"'.")); 47 | if (commandType.minVersion && commandType.minVersion > this.version) { 48 | throw new Error(`command ${type} incompatible with firmware version ${this.versionHex}`); 49 | } 50 | 51 | var cmdPromiseResolve = null; 52 | var cmdPromiseReject = null; 53 | var cmdPromise = new Promise( (resolve, reject) => { 54 | cmdPromiseResolve = resolve; 55 | cmdPromiseReject = reject; 56 | }); 57 | 58 | var cmd = Object.defineProperties({}, { 59 | type: {value: commandType, enumerable: true}, 60 | payload: {value: Buffer.alloc(0), writable:true}, 61 | options: {value: options}, 62 | cmdPromise: {value: cmdPromise}, 63 | cmdPromiseResolve: {value: cmdPromiseResolve}, 64 | cmdPromiseReject: {value: cmdPromiseReject}, 65 | status: {value: null, writable:true}, 66 | response: {value: null, writable:true}, 67 | timer: {value: null, writable: true}, 68 | [util.inspect.custom]: {value: function(depth, options) { 69 | var str = (""+this.type+"").red; 70 | for (var k in this) { 71 | if (k!=='type' && typeof(this[k]) !== 'function') { 72 | if (INSPECT_PRETTYFORMAT_FIELDS[k]) { 73 | str += ", " + (""+k) + ":" + ( ""+INSPECT_PRETTYFORMAT_FIELDS[k](this[k]) ).grey; 74 | } 75 | else { 76 | str += ", " + (""+k) + ":" + ( ""+this[k] ).grey; 77 | } 78 | } 79 | } 80 | return str; 81 | }}, 82 | }); 83 | 84 | commandType.build(options, cmd, this.version); 85 | return cmd; 86 | } 87 | } 88 | 89 | 90 | CommandBuilder.LOGS = { 91 | console: { trace: console.debug, debug: console.debug, info: console.log, warn: console.warn, error: console.error }, 92 | warn: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: console.warn, error: console.error }, 93 | error: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: console.error }, 94 | nolog: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: ()=>{}, }, 95 | }; 96 | 97 | module.exports = CommandBuilder; 98 | -------------------------------------------------------------------------------- /src/driver/buffer-reader.js: -------------------------------------------------------------------------------- 1 | // fork of original project: https://github.com/villadora/node-buffer-reader (BSD License) 2 | // simply added a 'isMore' function 3 | 4 | var assert = require('assert'); 5 | 6 | function BufferReader(buffer) { 7 | buffer = buffer || Buffer.alloc(0); 8 | assert(Buffer.isBuffer(buffer), 'A Buffer must be provided'); 9 | this.buf = buffer; 10 | this.offset = 0; 11 | } 12 | 13 | BufferReader.prototype.append = function(buffer) { 14 | assert(Buffer.isBuffer(buffer), 'A Buffer must be provided'); 15 | this.buf = Buffer.concat([this.buf, buffer]); 16 | return this; 17 | }; 18 | 19 | BufferReader.prototype.isMore = function() { 20 | return this.offset < this.buf.length; 21 | }; 22 | 23 | BufferReader.prototype.tell = function() { 24 | return this.offset; 25 | }; 26 | 27 | BufferReader.prototype.seek = function(pos) { 28 | assert(pos >= 0 && pos <= this.buf.length, 'Position is Invalid'); 29 | this.offset = pos; 30 | return this; 31 | }; 32 | 33 | BufferReader.prototype.move = function(diff) { 34 | assert(this.offset + diff >= 0 && this.offset + diff <= this.buf.length, 'Difference is Invalid'); 35 | this.offset += diff; 36 | return this; 37 | }; 38 | 39 | 40 | BufferReader.prototype.nextAll = 41 | BufferReader.prototype.restAll = function() { 42 | var remain = this.buf.length - this.offset; 43 | assert(remain >= 0, 'Buffer is not in normal state: offset > totalLength'); 44 | var buf = Buffer.alloc(remain); 45 | this.buf.copy(buf, 0, this.offset); 46 | this.offset = this.buf.length; 47 | return buf; 48 | }; 49 | 50 | 51 | BufferReader.prototype.nextBuffer = function(length) { 52 | assert(length >= 0, 'Length must be no negative'); 53 | assert(this.offset + length <= this.buf.length, "Out of Original Buffer's Boundary"); 54 | var buf = Buffer.alloc(length); 55 | this.buf.copy(buf, 0, this.offset, this.offset + length); 56 | this.offset += length; 57 | return buf; 58 | }; 59 | 60 | BufferReader.prototype.nextString = function(length, encoding) { 61 | assert(length >= 0, 'Length must be no negative'); 62 | assert(this.offset + length <= this.buf.length, "Out of Original Buffer's Boundary"); 63 | 64 | this.offset += length; 65 | return this.buf.toString(encoding, this.offset - length, this.offset); 66 | }; 67 | 68 | BufferReader.prototype.nextStringZero = function(encoding) { 69 | // Find null by end of buffer 70 | for(var length = 0; length + this.offset < this.buf.length && this.buf[this.offset + length] !== 0x00; length++) ; 71 | 72 | assert(length <= this.buf.length && this.buf[this.offset + length] === 0x00, "Out of Original Buffer's Boundary"); 73 | 74 | this.offset += length + 1; 75 | return this.buf.toString(encoding, this.offset - length - 1, this.offset - 1); 76 | }; 77 | 78 | 79 | function MAKE_NEXT_READER(valueName, size) { 80 | valueName = cap(valueName); 81 | BufferReader.prototype['next' + valueName] = function() { 82 | assert(this.offset + size <= this.buf.length, "Out of Original Buffer's Boundary"); 83 | var val = this.buf['read' + valueName](this.offset); 84 | this.offset += size; 85 | return val; 86 | }; 87 | } 88 | 89 | function MAKE_NEXT_READER_BOTH(valueName, size) { 90 | MAKE_NEXT_READER(valueName + 'LE', size); 91 | MAKE_NEXT_READER(valueName + 'BE', size); 92 | } 93 | 94 | MAKE_NEXT_READER('Int8', 1); 95 | MAKE_NEXT_READER('UInt8', 1); 96 | MAKE_NEXT_READER_BOTH('UInt16', 2); 97 | MAKE_NEXT_READER_BOTH('Int16', 2); 98 | MAKE_NEXT_READER_BOTH('UInt32', 4); 99 | MAKE_NEXT_READER_BOTH('Int32', 4); 100 | MAKE_NEXT_READER_BOTH('Float', 4); 101 | MAKE_NEXT_READER_BOTH('Double', 8); 102 | 103 | function cap(str) { 104 | return str.charAt(0).toUpperCase() + str.slice(1); 105 | } 106 | 107 | 108 | module.exports = BufferReader; 109 | -------------------------------------------------------------------------------- /src/coordinator/value.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const EventEmitter = require('events').EventEmitter; 3 | const Sym = require('./symbols.js'); 4 | 5 | class Value extends EventEmitter { 6 | constructor(id, device, valuedef) { 7 | super(); 8 | 9 | if (!id) throw new Error("no id provided"); 10 | if (!device) throw new Error("no device provided"); 11 | if (!valuedef) throw new Error("no definition provided"); 12 | 13 | this[Sym.ID] = id; 14 | this[Sym.TYPE] = ''; 15 | this[Sym.DEVICE] = device; 16 | this[Sym.DEFINITION] = valuedef; 17 | this[Sym.VALUE_DATA] = undefined; 18 | this[Sym.VALUE_CB] = {}; 19 | this[Sym.VALUE_BOUND_ATTR] = null; 20 | } 21 | 22 | get id() { return this[Sym.ID]; } 23 | get definition() { return this[Sym.DEFINITION]; } 24 | get type() { return this[Sym.TYPE]; } 25 | get device() { return this[Sym.DEVICE]; } 26 | get log() { return this.device.log; } 27 | 28 | get value() { return this[Sym.VALUE_DATA]; } 29 | set value(val) { return this.setValue(val); } 30 | setValue(newval) { return this.device[Sym.COORDINATOR].setValue(this, newval, true); } // true = update bound attribute 31 | 32 | [Sym.SETUP]() { 33 | let def = this[Sym.DEFINITION]; 34 | this.log.debug('setup of value '+this+' with ('+JSON.stringify(def)+')...'); 35 | 36 | if (def.type) this[Sym.TYPE] = def.type; 37 | 38 | let initialValue = def.default; 39 | 40 | // setup attribute binding 41 | if (def.attribute) { 42 | let attrdef = /^\W*((?:0x)?[0-9a-fA-F]+)\W+((?:0x)?[0-9a-fA-F]+)\W+((?:0x)?[0-9a-fA-F]+)\W*$/gi.exec(def.attribute.id) 43 | if (!attrdef) throw new Error("invalid attribue for value "+this.id+": attribute id = '"+def.attribute.id+"'"); 44 | let endpointId = parseInt(attrdef[1]); 45 | let clusterId = parseInt(attrdef[2]); 46 | let attributeId = parseInt(attrdef[3]); 47 | 48 | // initial setup of bound attribute 49 | let attribute = this.device.attribute(endpointId, clusterId, attributeId); 50 | if (attribute) { 51 | // attribute already exists 52 | this[Sym.VALUE_BOUND_ATTR] = attribute; 53 | let res = (def.attribute.toValue) ? def.attribute.toValue(attribute.value, this) : attribute.value; 54 | initialValue = res; 55 | } 56 | else { 57 | // attribute doesn't exist yet ; wait for its addition 58 | this[Sym.VALUE_CB]['attribute_add'] = (attribute) => { 59 | if (this[Sym.VALUE_BOUND_ATTR]) return; // already bound ; skip. 60 | else if (attribute.id === attributeId && attribute.cluster.id === clusterId && attribute.endpoint.id === endpointId) { 61 | this[Sym.VALUE_BOUND_ATTR] = attribute; 62 | this[Sym.VALUE_CB]['attribute_change'](attribute, attribute.value); 63 | } 64 | }; 65 | } 66 | 67 | // monitor attributes changes and reflect to this value. 68 | this[Sym.VALUE_CB]['attribute_change'] = (attribute, newval, oldval) => { 69 | if (attribute === this[Sym.VALUE_BOUND_ATTR]) { 70 | let res = (def.attribute.toValue) ? def.attribute.toValue(newval, this) : newval; 71 | this.device[Sym.COORDINATOR].setValue(this, res, false) // false = don't update bound attribute 72 | } 73 | }; 74 | } 75 | 76 | // bind all callbacks. 77 | Object.entries(this[Sym.VALUE_CB]).forEach( ([key,fn]) => { this.device.on(key, fn) }); 78 | 79 | this[Sym.VALUE_DATA] = initialValue; 80 | } 81 | 82 | [Sym.DESTROY]() { 83 | Object.entries(this[Sym.VALUE_CB]).forEach( ([key,fn]) => { 84 | //this.log.debug(''+this+' stops listening for events "'+key+'" of '+this.device); 85 | this.device.removeListener(key, fn) 86 | }); 87 | this[Sym.VALUE_CB] = {}; 88 | this[Sym.VALUE_DATA] = undefined; 89 | this[Sym.VALUE_BOUND_ATTR] = null; 90 | } 91 | 92 | [Sym.SET_VALUE_DATA](newval) { 93 | let oldval = this[Sym.VALUE_DATA]; 94 | this[Sym.VALUE_DATA] = newval; 95 | this.log.debug(''+this.device+''+this+' value changed ('+JSON.stringify(oldval)+') => ('+JSON.stringify(newval)+')'); 96 | this.emit('value_change', this, newval, oldval); 97 | } 98 | 99 | toString() { return "[value_"+this.id+"]"; } 100 | [util.inspect.custom](depth, opts) { 101 | return ""+this+"="+this[Sym.VALUE_DATA]+" ("+JSON.stringify(this.definition)+")"; 102 | } 103 | } 104 | 105 | module.exports = Value; 106 | -------------------------------------------------------------------------------- /src/driver/clusters/genPowerCfg.js: -------------------------------------------------------------------------------- 1 | batterySizeEnum = { 2 | 0x00: 'no battery', 3 | 0x01: 'built in', 4 | 0x02: 'other', 5 | 0x03: 'AA', 6 | 0x04: 'AAA', 7 | 0x05: 'C', 8 | 0x06: 'D', 9 | 0x07: 'CR2', 10 | 0x08: 'CR123A', 11 | 0xff: 'unknown', 12 | }; 13 | 14 | batteryAlarmBits = { 15 | 0: 'battery too low', 16 | 1: 'battery threshold 1', 17 | 2: 'battery threshold 2', 18 | 3: 'battery threshold 3', 19 | }; 20 | 21 | 22 | module.exports = { 23 | "id": 1, 24 | "name": "genPowerCfg", 25 | "specific": null, 26 | "attributes": { 27 | "0": { "cluster": 1, "id": 0, "name": "mainsVoltage", "type": "uint16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '100mV' }, 28 | "1": { "cluster": 1, "id": 1, "name": "mainsFrequency", "type": "uint8", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '2Hz' }, 29 | "16": { "cluster": 1, "id": 16, "name": "mainsAlarmMask", "type": "bitmap8", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null }, 30 | "17": { "cluster": 1, "id": 17, "name": "mainsVoltMinThreshold", "type": "uint16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '100mV' }, 31 | "18": { "cluster": 1, "id": 18, "name": "mainsVoltMaxThreshold", "type": "uint16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '100mV' }, 32 | "19": { "cluster": 1, "id": 19, "name": "mainsVoltageDwellTripPoint", "type": "uint16", "mandatory": false, "read": true, "write": false, "specific": false, "unit": 'second' }, 33 | "32": { "cluster": 1, "id": 32, "name": "batteryVoltage", "type": "uint8", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '100mV' }, 34 | "33": { "cluster": 1, "id": 33, "name": "batteryPercentageRemaining", "type": "uint8", "mandatory": false, "read": true, "write": false, "specific": false, "unit": '0.5%', default:0 }, 35 | "48": { "cluster": 1, "id": 48, "name": "batteryManufacturer", "type": "string", "mandatory": null, "read": true, "write": true, "specific": false, "unit": null, default:'' }, 36 | "49": { "cluster": 1, "id": 49, "name": "batterySize", "type": "enum8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": null, default:0xff, enum: batterySizeEnum }, 37 | "50": { "cluster": 1, "id": 50, "name": "batteryAHrRating", "type": "uint16", "mandatory": null, "read": true, "write": true, "specific": false, "unit": '10mAh' }, 38 | "51": { "cluster": 1, "id": 51, "name": "batteryQuantity", "type": "uint8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": null }, 39 | "52": { "cluster": 1, "id": 52, "name": "batteryRatedVoltage", "type": "uint8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": '100mV' }, 40 | "53": { "cluster": 1, "id": 53, "name": "batteryAlarmMask", "type": "bitmap8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": null, default:0, bits: batteryAlarmBits }, 41 | "54": { "cluster": 1, "id": 54, "name": "batteryVoltMinThres", "type": "uint8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": '100mV', default:0 }, 42 | "55": { "cluster": 1, "id": 55, "name": "batteryVoltThres1", "type": "uint8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": '100mV', default:0 }, 43 | "56": { "cluster": 1, "id": 56, "name": "batteryVoltThres2", "type": "uint8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": '100mV', default:0 }, 44 | "57": { "cluster": 1, "id": 57, "name": "batteryVoltThres3", "type": "uint8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": '100mV', default:0 }, 45 | "58": { "cluster": 1, "id": 58, "name": "batteryPercentMinThreshold", "type": "uint8", "mandatory": null, "read": true, "write": true, "specific": false, "unit": '%', default:0 }, 46 | "59": { "cluster": 1, "id": 59, "name": "batteryPercentThreshold1", "type": "uint8", "mandatory": null, "read": null, "write": null, "specific": false, "unit": '%', default:0 }, 47 | "60": { "cluster": 1, "id": 60, "name": "batteryPercentThreshold2", "type": "uint8", "mandatory": null, "read": null, "write": null, "specific": false, "unit": '%', default:0 }, 48 | "61": { "cluster": 1, "id": 61, "name": "batteryPercentThreshold3", "type": "uint8", "mandatory": null, "read": null, "write": null, "specific": false, "unit": '%', default:0 }, 49 | "62": { "cluster": 1, "id": 62, "name": "batteryAlarmState", "type": "bitmap32", "mandatory": null, "read": true, "write": false, "specific": false, "unit": null, default:0 } 50 | }, 51 | "commands": {}, 52 | "responses": {}, 53 | }; 54 | -------------------------------------------------------------------------------- /src/driver/responseBuilder.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const BufferReader = require('./buffer-reader.js'); 5 | const Enum = require('./enum.js'); 6 | const colors = require('colors'); 7 | const VERSION = Symbol('VERSION'); 8 | 9 | Enum.create('RESPONSES'); 10 | 11 | const INSPECT_PRETTYFORMAT_FIELDS = { 12 | timestamp: function(timestamp, cmd) { return ""+timestamp.getFullYear()+'-'+(timestamp.getMonth()+1)+'-'+timestamp.getDate()+' '+timestamp.getHours()+':'+timestamp.getMinutes()+':'+timestamp.getSeconds()+'.'+timestamp.getMilliseconds(); }, 13 | address: function(address, cmd) { return '0x'+address.toString(16); }, 14 | value: function(value, cmd) { return JSON.stringify(value); }, 15 | definition: function(definition, cmd) { return definition && definition.name ? definition.name : 'unknown'; }, 16 | status: function(value, cmd) { return JSON.stringify(value); }, 17 | devices: function(value, cmd) { 18 | let str = ""; 19 | value.forEach(d => { 20 | str += str ? ", {": "{"; 21 | for (var k in d) { 22 | if (k!=='type' && typeof(d[k]) !== 'function') { 23 | if (INSPECT_PRETTYFORMAT_FIELDS[k]) { 24 | var strval = INSPECT_PRETTYFORMAT_FIELDS[k](d[k]); 25 | str += ", "+(""+k)+":"+(strval ? strval.grey : strval); 26 | } 27 | else { 28 | str += ", "+(""+k)+":"+( ""+d[k] ).grey; 29 | } 30 | } 31 | } 32 | str +="}"; 33 | }); 34 | return str; 35 | } 36 | } 37 | 38 | class ResponseBuilder { 39 | constructor(options) { 40 | } 41 | 42 | loadResponses(repDir) { 43 | Enum.RESPONSES.clear(); 44 | 45 | repDir = repDir || __dirname +'/responses'; 46 | 47 | var fileList = fs.readdirSync(path.resolve(repDir)); 48 | fileList.forEach((id) => { 49 | var repPath = path.resolve(repDir + "/" + id); 50 | try { 51 | var rep = require(repPath); 52 | rep.typeHex = rep.id.toString(16); 53 | } catch (e) { 54 | console.error("exception while loading response '" + id + "'."); 55 | throw e; 56 | } 57 | Enum.RESPONSES.add(rep); 58 | }); 59 | } 60 | 61 | set version(version) { this[VERSION] = version; } 62 | get version() { return this[VERSION]; } 63 | get versionHex() { return this[VERSION] && (this[VERSION]).toString(16); } 64 | 65 | parse(typeid,payload) { 66 | var responseType = Enum.RESPONSES(typeid, new Error("invalid response typeid '"+typeid+"'.")); 67 | 68 | var reader = new BufferReader(payload); 69 | var rep = Object.defineProperties({}, { 70 | type: {value: responseType, enumerable: true}, 71 | payload: {value: payload}, 72 | reader: {value: reader}, 73 | [util.inspect.custom]: {value: function(depth, options) { 74 | var str = (""+this.type+"").green; 75 | for (var k in this) { 76 | if (k!=='type' && typeof(this[k]) !== 'function') { 77 | if (INSPECT_PRETTYFORMAT_FIELDS[k]) { 78 | var strval = INSPECT_PRETTYFORMAT_FIELDS[k](this[k]); 79 | str += ", "+(""+k)+":"+(strval ? strval.grey : strval); 80 | } 81 | else { 82 | str += ", "+(""+k)+":"+( ""+this[k] ).grey; 83 | } 84 | } 85 | } 86 | return str; 87 | }}, 88 | }); 89 | 90 | try { 91 | responseType.parse(reader, rep, this.version); 92 | 93 | if (reader.isMore()) { 94 | ResponseBuilder.LOGS.warn("[ResponseBuilder_"+responseType+"] the "+(payload.length - reader.tell())+" last bytes of data have not been parsed:"); 95 | ResponseBuilder.LOGS.warn("[ResponseBuilder_"+responseType+"] response payload: " 96 | + (payload.toString('hex').slice(0, reader.tell()).replace(/../g, "$& ")).green 97 | + (payload.toString('hex').slice(reader.tell()).replace(/../g, "$& ")).red 98 | ); 99 | } 100 | return rep; 101 | } 102 | catch (e) { 103 | ResponseBuilder.LOGS.error("[ResponseBuilder_"+responseType+"] payload = "+payload.toString('hex').replace(/../g, "$& ")); 104 | ResponseBuilder.LOGS.error("[ResponseBuilder_"+responseType+"] EXCEPTION while parsing response:"); 105 | ResponseBuilder.LOGS.error("[ResponseBuilder_"+responseType+"] EXCEPTION while parsing response: "+e.stack); 106 | throw e; 107 | } 108 | 109 | } 110 | 111 | } 112 | 113 | ResponseBuilder.LOGS = { 114 | console: { trace: console.debug, debug: console.debug, info: console.log, warn: console.warn, error: console.error }, 115 | warn: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: console.warn, error: console.error }, 116 | error: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: console.error }, 117 | nolog: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: ()=>{}, }, 118 | }; 119 | module.exports = ResponseBuilder; 120 | -------------------------------------------------------------------------------- /devices/osram.smart_plus_plug.js: -------------------------------------------------------------------------------- 1 | let ELIGIBLE_MODEL_ID = ['Plug 01']; 2 | 3 | module.exports = { 4 | 5 | id: 'osram_smart_plus_plug', 6 | 7 | match: function(device) { 8 | let modelId = device.attribute(3, 0, 5) && device.attribute(3, 0, 5).value; 9 | if (ELIGIBLE_MODEL_ID.includes(modelId)) return 1000; 10 | }, 11 | 12 | values: { 13 | modelId: { type:'string', attribute: {id: '0x0003 0x0000 0x0005' } }, 14 | state: { type:'bool', attribute: {id: '0x0003 0x0006 0x0000' } }, 15 | }, 16 | 17 | actions: { 18 | on: { exec: function() { return this.device.send('action_onoff', { address:this.device.address, endpoint: 0x03, on:true }); } }, 19 | off: { exec: function() { return this.device.send('action_onoff', { address:this.device.address, endpoint: 0x03, off:true }); } }, 20 | toggle: { exec: function() { return this.device.send('action_onoff', { address:this.device.address, endpoint: 0x03, toggle:true }); } }, 21 | }, 22 | 23 | events: { 24 | switch_on: { attribute: { id: '0x0003 0x0006 0X0000', equal: true } }, 25 | switch_off: { attribute: { id: '0x0003 0x0006 0X0000', equal: false } }, 26 | 27 | }, 28 | 29 | }; 30 | 31 | /* 32 | 33 | sending command: active_endpoint(0x45), address:0x6e25, timestamp:2018-7-11 16:28:12.775 34 | response received: active_endpoint(0x8045), sequence:106, status:{"id":0,"name":"success","description":"Command was successful"}, address:0x6e25, endpoints:3, rssi:72 35 | 36 | sending command: descriptor_simple(0x43), address:0x6e25, endpoint:3, timestamp:2018-7-11 16:30:10.266 37 | response received: descriptor_simple(0x8043), sequence:107, status:0, address:0x6e25, length:26, endpoint:3, profile:ll(0xc05e), seviceType:[object Object], deviceVersion:2, reserved:0, inClusters:4096,0,3,4,5,6,2820,64527, outClusters:25, rssi:75 38 | 39 | sending command: descriptor_power(0x44), address:0x6e25, timestamp:2018-7-11 16:34:18.781 40 | descriptor_power(0x8044), sequence:108, status:0, powerMode:0, availableSource:14, currentSource:2, currentLevel:12, rssi:75 41 | 42 | sending command: descriptor_node(0x42), address:0x6e25, timestamp:2018-7-11 16:35:21.872 43 | response received: descriptor_node(0x8042), sequence:109, address:0x6e, manufacturer:9659, maxRxSize:43520, maxTxSize:0, serverFlags:[object Object], alternatePanCoordinator:false, deviceType:false, powerSource:false, receiverOnWhenIdle:false, securityCapability:true, allocateAddress:true, maxBufferSize:142, nodeType:end_device(0x2), complexDescriptorAvailable:false, userDescriptorAvailable:false, reserved:0, apsFlags:2, frequencyBand:0, rssi:75 44 | 45 | 46 | inClusters:4096,0,3,4,5,6,2820,64527, outClusters:25 47 | 48 | 49 | 50 | // 01 81 40 complete 00 type 05 id af 00 20 00 00 4b 51 | 52 | zigate.zigate.driver.send('attribute_discovery', {address:0x6e25, endpoint:3, cluster:0, firstId:18}) 53 | 54 | cluster 0: genBasic 55 | type = 0x20 id = 0x00 56 | type = 0x20 id = 0x01 57 | type = 0x20 id = 0x02 58 | type = 0x20 id = 0x03 59 | type = 0x42 id = 0x04 60 | type = 0x42 id = 0x05 61 | type = 0x42 id = 0x06 62 | type = 0x30 id = 0x07 63 | type = 0x10 id = 0x12 64 | type = 0x42 id = 0x4000 65 | 66 | cluster 3: genDeviceTempCfg 67 | type = 0x21 id = 0x00 68 | 69 | cluster 4: genGroups 70 | type = 0x18 id = 0x00 71 | 72 | cluster 5: genScenes 73 | type = 0x20 id = 0x00 74 | type = 0x20 id = 0x01 75 | type = 0x21 id = 0x02 76 | type = 0x02 id = 0x03 77 | type = 0x18 id = 0x04 78 | 79 | cluster 6: genOnOff 80 | type = 0x10 id = 0x00 RO Mandatory bool OnOff 81 | type = 0x10 id = 0x4000 RO Optional bool GlobalSceneControl 82 | type = 0x21 id = 0x4001 RW Optional uint16 OnTime 1/10 second 83 | type = 0x21 id = 0x4002 RW Optional uint16 OffWaitTime 1/10 second 84 | 85 | cluster 2820: 0xb04 genElectricalMeasurement 86 | ???? 87 | 88 | cluster 64527: 0xfc0f 89 | ???? 90 | 91 | out cluster 25: 0x 19 (with directon =1 ; cli_to_srv) OTA Upgrade 92 | type = 0xfo id = 0x00 93 | type = 0x23 id = 0x01 94 | type = 0x23 id = 0x02 95 | type = 0x21 id = 0x03 96 | type = 0x23 id = 0x04 97 | type = 0x21 id = 0x05 98 | type = 0x30 id = 0x06 99 | type = 0x21 id = 0x07 100 | type = 0x21 id = 0x08 101 | type = 0x21 id = 0x09 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 112 | From zigate.fr 113 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 114 | 115 | Clusters disponibles 116 | 117 | EndPoint: 0x03 118 | Profile ID: 0xC05E (ZigBee LL) 119 | Device ID: 0x0010 (Unknown) 120 | Input Cluster Count: 8 121 | Cluster 0x0000 (General: Basic) 122 | Cluster 0x0003 (General: Identify) 123 | Cluster 0x0004 (General: Groups) 124 | Cluster 0x0005 (General: Scenes) 125 | Cluster 0x0006 (General: On/Off) 126 | Cluster 0x1000 (ZLL: Commissioning) 127 | Cluster 0x0B04 (Unknown) 128 | Cluster 0xFC0F (Unknown) 129 | 130 | Output Cluster Count: 1 131 | Cluster 0: Cluster ID: 0x1000 (ZLL: Commissioning) 132 | 133 | */ 134 | -------------------------------------------------------------------------------- /src/coordinator/device.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const EventEmitter = require('events').EventEmitter; 3 | const Sym = require('./symbols.js'); 4 | const ZiEndpoint = require('./ziendpoint.js'); 5 | const DeviceTypes = require('./deviceTypes.js'); 6 | 7 | class Device extends EventEmitter { 8 | constructor(coordinator, address) { 9 | super(); 10 | this[Sym.ADDRESS] = address; 11 | this[Sym.TYPE] = DeviceTypes.Default; 12 | this[Sym.COORDINATOR] = coordinator; 13 | this[Sym.IEEE] = null; 14 | this[Sym.READY] = false; 15 | this[Sym.ENDPOINTS] = {}; 16 | this[Sym.VALUES] = {}; 17 | this[Sym.ACTIONS] = {}; 18 | this[Sym.EVENTS] = {}; 19 | this[Sym.BATTERY] = undefined; 20 | } 21 | send(cmdname, options) { return this[Sym.COORDINATOR].send(cmdname, options); } 22 | get address() { return this[Sym.ADDRESS]; } 23 | get hex() { return "0x"+(("0000"+Number(this.address).toString(16)).substr(-4,4)); } 24 | get ieee() { return this[Sym.IEEE]; } 25 | set ieee(ieee) { if (this[Sym.IEEE] !== null) throw new Error(""+this+" ieee already set."); else this[Sym.IEEE] = ieee; } 26 | get type() { return this[Sym.TYPE].id; } 27 | get battery() { return this[Sym.BATTERY]; } 28 | set battery(isbattery) { return this[Sym.COORDINATOR].setDeviceBattery(this, isbattery); } 29 | 30 | get endpoints() { return Object.values(this[Sym.ENDPOINTS]); } 31 | endpoint(id) { return this[Sym.ENDPOINTS][id]; } 32 | addEndpoint(id, verified) { return this[Sym.COORDINATOR].addEndpoint(this, id, verified); } 33 | queryEndpoints() { return this[Sym.COORDINATOR].queryEndpoints(this); } 34 | 35 | get attributes() { 36 | var attrs = []; 37 | Object.keys(this[Sym.ENDPOINTS]).forEach( (eid)=> { 38 | var endpoint = this[Sym.ENDPOINTS][eid]; 39 | Object.keys(this[Sym.ENDPOINTS][eid].clusters).forEach( (cid)=> { 40 | var cluster = endpoint.clusters[cid]; 41 | Object.keys(cluster.attributes).forEach( (aid)=> { 42 | var attr = cluster.attributes[aid]; 43 | attrs.push(attr); 44 | }); 45 | }); 46 | }); 47 | return attrs; 48 | } 49 | 50 | attribute(endpoint, cluster, attribute) { 51 | let edp = this.endpoint(endpoint); 52 | if (edp) { 53 | let clu = edp.cluster(cluster); 54 | if (clu) { 55 | return clu.attribute(attribute); 56 | } 57 | } 58 | return null; 59 | } 60 | 61 | get values() { return Object.values(this[Sym.VALUES]); } 62 | value(id) { return this[Sym.VALUES][id]; } 63 | addValue(id, definition) { this[Sym.COORDINATOR].addValue(this, id, definition); } 64 | removeValue(id) { this[Sym.COORDINATOR].removeValue(this, id); } 65 | 66 | get actions() { return Object.values(this[Sym.ACTIONS]); } 67 | action(id) { return this[Sym.ACTIONS][id]; } 68 | addAction(id, definition) { this[Sym.COORDINATOR].addAction(this, id, definition); } 69 | removeAction(id) { this[Sym.COORDINATOR].removeAction(this, id); } 70 | 71 | get events() { return Object.values(this[Sym.EVENTS]); } 72 | event(id) { return this[Sym.EVENTS][id]; } 73 | addEvent(id, definition) { this[Sym.COORDINATOR].addEvent(this, id, definition); } 74 | removeEvent(id) { this[Sym.COORDINATOR].removeEvent(this, id); } 75 | 76 | 77 | [Sym.ON_ENDPOINT_ADD](endpoint) { 78 | this.emit('endpoint_add', endpoint); 79 | } 80 | [Sym.ON_CLUSTER_ADD](cluster) { 81 | this.emit('cluster_add', cluster); 82 | } 83 | [Sym.ON_ATTRIBUTE_ADD](attribute) { 84 | this.emit('attribute_add', attribute); 85 | } 86 | [Sym.ON_ATTRIBUTE_CHANGE](attribute, newval, oldval) { 87 | this.emit('attribute_change', attribute, newval, oldval); 88 | } 89 | [Sym.ON_COMMAND_ADD](command) { 90 | this.emit('command_add', command); 91 | } 92 | [Sym.ON_VALUE_ADD](value) { 93 | this.emit('value_add', value); 94 | } 95 | [Sym.ON_VALUE_REMOVE](value) { 96 | this.emit('value_remove', value); 97 | } 98 | [Sym.ON_VALUE_CHANGE](value, newval, oldval) { 99 | this.emit('value_change', value, newval, oldval); 100 | } 101 | [Sym.ON_ACTION_ADD](action) { 102 | this.emit('action_add', action); 103 | } 104 | [Sym.ON_ACTION_REMOVE](action) { 105 | this.emit('action_remove', action); 106 | } 107 | [Sym.ON_ACTION_EXEC](action, args, ret) { 108 | this.emit('action_exec', action, args, ret); 109 | } 110 | [Sym.ON_EVENT_ADD](event) { 111 | this.emit('event_add', event); 112 | } 113 | [Sym.ON_EVENT_REMOVE](action) { 114 | this.emit('event_remove', action); 115 | } 116 | [Sym.ON_EVENT_FIRE](event, args) { 117 | this.emit(event.id, args); 118 | } 119 | [Sym.ON_TYPE_CHANGE](newtypename, oldtypename) { 120 | this.emit('type_change', newtypename, oldtypename); 121 | } 122 | [Sym.DESTROY]() { 123 | this.emit('device_remove', this); 124 | } 125 | [Sym.ON_BATTERY_CHANGE](newval, oldval) { 126 | this.emit('battery_change', newval, oldval); 127 | } 128 | 129 | 130 | get log() { return this[Sym.COORDINATOR].log; } 131 | toString() { return "[device_0x"+this[Sym.ADDRESS].toString(16)+"]"; } 132 | [util.inspect.custom](depth, opts) { 133 | let out = ''+this+' ('+this.type+')\n'; 134 | this.endpoints.forEach(e => { 135 | out += ' '+e+'\n'; 136 | e.clusters.forEach((c) => { 137 | out += ' '+c+'\n'; 138 | c.attributes.forEach((a) => { out += ' '+a[util.inspect.custom]()+'\n'}); 139 | c.commands.forEach((c) => { out += ' '+c+'\n'}); 140 | }); 141 | }); 142 | this.values.forEach((v) => { out += ' '+util.inspect(v)+'\n'}); 143 | this.actions.forEach((a) => { out += ' '+util.inspect(a)+'\n'}); 144 | this.events.forEach((e) => { out += ' '+util.inspect(e)+'\n'}); 145 | 146 | return out; 147 | } 148 | } 149 | 150 | 151 | module.exports = Device; 152 | -------------------------------------------------------------------------------- /src/driver/clusters/genBasic.js: -------------------------------------------------------------------------------- 1 | const powerSourceEnum = { 2 | 0x00: 'unknown', 3 | 0x01: 'mains (single phase)', 4 | 0x02: 'mains (3 phase)', 5 | 0x03: 'battery', 6 | 0x04: 'DC source', 7 | 0x05: 'emergency mains constantly powered', 8 | 0x06: 'emergency mains and transfer switch', 9 | }; 10 | 11 | const physicalEnvEnum = { 12 | 0x00: 'Unspecified environment', 13 | 0x01: 'Mirror (ZSE Profile)', 14 | 0x01: 'Atrium', 15 | 0x02: 'Bar', 16 | 0x03: 'Courtyard', 17 | 0x04: 'Bathroom', 18 | 0x05: 'Bedroom', 19 | 0x06: 'Billiard Room', 20 | 0x07: 'Utility Room', 21 | 0x08: 'Cellar', 22 | 0x09: 'Storage Closet', 23 | 0x0a: 'Theater', 24 | 0x0b: 'Office', 25 | 0x0c: 'Deck', 26 | 0x0d: 'Den', 27 | 0x0e: 'Dining Room', 28 | 0x0f: 'Electrical Room', 29 | 0x10: 'Elevator', 30 | 0x11: 'Entry', 31 | 0x12: 'Family Room', 32 | 0x13: 'Main Floor', 33 | 0x14: 'Upstairs', 34 | 0x15: 'Downstairs', 35 | 0x16: 'Basement/Lower Level', 36 | 0x17: 'Gallery',0x18: 'Game Room', 37 | 0x19: 'Garage', 38 | 0x1a: 'Gym', 39 | 0x1b: 'Hallway', 40 | 0x1c: 'House', 41 | 0x1d: 'Kitchen', 42 | 0x1e: 'Laundry Room', 43 | 0x1f: 'Library', 44 | 0x20: 'Master Bedroom', 45 | 0x21: 'Mud Room (small room for coats and boots)', 46 | 0x22: 'Nursery', 47 | 0x23: 'Pantry', 48 | 0x24: 'Office', 49 | 0x25: 'Outside', 50 | 0x26: 'Pool', 51 | 0x27: 'Porch', 52 | 0x28: 'Sewing Room', 53 | 0x29: 'Sitting Room', 54 | 0x2a: 'Stairway', 55 | 0x2b: 'Yard', 56 | 0x2c: 'Attic', 57 | 0x2d: 'Hot Tub', 58 | 0x2e: 'Living Room', 59 | 0x2f: 'Sauna', 60 | 0x30: 'Shop/Workshop', 61 | 0x31: 'Guest Bedroom', 62 | 0x32: 'Guest Bath', 63 | 0x33: 'Powder Room (1/2 bath)', 64 | 0x34: 'Back Yard', 65 | 0x35: 'Front Yard', 66 | 0x36: 'Patio', 67 | 0x37: 'Driveway', 68 | 0x38: 'Sun Room', 69 | 0x39: 'Living Room', 70 | 0x3a: 'Spa', 71 | 0x3b: 'Whirlpool', 72 | 0x3c: 'Shed', 73 | 0x3d: 'Equipment Storage', 74 | 0x3e: 'Hobby/Craft Room', 75 | 0x3f: 'Fountain', 76 | 0x40: 'Pond', 77 | 0x41: 'Reception Room', 78 | 0x42: 'Breakfast Room', 79 | 0x43: 'Nook', 80 | 0x44: 'Garden', 81 | 0x45: 'Balcony', 82 | 0x46: 'Panic Room', 83 | 0x47: 'Terrace', 84 | 0x48: 'Roof', 85 | 0x49: 'Toilet', 86 | 0x4a: 'Toilet Main', 87 | 0x4b: 'Outside Toilet', 88 | 0x4c: 'Shower room', 89 | 0x4d: 'Study', 90 | 0x4e: 'Front Garden', 91 | 0x4f: 'Back Garden', 92 | 0x50: 'Kettle', 93 | 0x51: 'Television', 94 | 0x52: 'Stove', 95 | 0x53: 'Microwave', 96 | 0x54: 'Toaster', 97 | 0x55: 'Vacuum', 98 | 0x56: 'Appliance', 99 | 0x57: 'Front Door', 100 | 0x58: 'Back Door', 101 | 0x59: 'Fridge Door', 102 | 0x60: 'Medication Cabinet Door', 103 | 0x61: 'Wardrobe Door', 104 | 0x62: 'Front Cupboard Door', 105 | 0x63: 'Other Door', 106 | 0x64: 'Waiting Room', 107 | 0x65: 'Triage Room', 108 | 0x66: 'Doctor’s Office', 109 | 0x67: 'Patient’s Private Room', 110 | 0x68: 'Consultation Room', 111 | 0x69: 'Nurse Station', 112 | 0x6a: 'Ward', 113 | 0x6b: 'Corridor', 114 | 0x6c: 'Operating Theatre', 115 | 0x6d: 'Dental Surgery Room', 116 | 0x6e: 'Medical Imaging Room', 117 | 0x6f: 'Decontamination Room', 118 | 0xff: 'Unknown environment', 119 | }; 120 | module.exports = { 121 | "id": 0, 122 | "name": "genBasic", 123 | "specific": false, 124 | "attributes": { 125 | "0": { "cluster": 0, "id": 0, "name": "zclVersion", "type": "uint8", "mandatory": true, "read": true, "write": false, "specific": false, "unit": null, default:0x2 }, 126 | "1": { "cluster": 0, "id": 1, "name": "appVersion", "type": "uint8", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null, default:0x0 }, 127 | "2": { "cluster": 0, "id": 2, "name": "stackVersion", "type": "uint8", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null, default:0x0 }, 128 | "3": { "cluster": 0, "id": 3, "name": "hwVersion", "type": "uint8", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null, default:0x0 }, 129 | "4": { "cluster": 0, "id": 4, "name": "manufacturerName", "type": "string", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null, default:'' }, 130 | "5": { "cluster": 0, "id": 5, "name": "modelId", "type": "string", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null, default:'' }, 131 | "6": { "cluster": 0, "id": 6, "name": "dateCode", "type": "string", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null, default:'' }, 132 | "7": { "cluster": 0, "id": 7, "name": "powerSource", "type": "enum8", "mandatory": true, "read": true, "write": false, "specific": false, "unit": null, default:0x0, enum: powerSourceEnum }, 133 | "8": { "cluster": 0, "id": 8, "name": "appProfileVersion", "type": "enum8", "mandatory": null, "read": null, "write": null, "specific": null, "unit": null }, 134 | "16": { "cluster": 0, "id": 16, "name": "locationDesc", "type": "string", "mandatory": false, "read": true, "write": true, "specific": false, "unit": null, default:'' }, 135 | "17": { "cluster": 0, "id": 17, "name": "physicalEnv", "type": "enum8", "mandatory": false, "read": true, "write": true, "specific": false, "unit": null, default:0x0, enum: physicalEnvEnum }, 136 | "18": { "cluster": 0, "id": 18, "name": "deviceEnabled", "type": "boolean", "mandatory": false, "read": true, "write": true, "specific": false, "unit": null, default:0x1 }, 137 | "19": { "cluster": 0, "id": 19, "name": "alarmMask", "type": "bitmap8", "mandatory": false, "read": true, "write": true, "specific": false, "unit": null, default:0x0 }, 138 | "20": { "cluster": 0, "id": 20, "name": "disableLocalConfig", "type": "bitmap8", "mandatory": false, "read": true, "write": true, "specific": false, "unit": null, default:0x0 }, 139 | "16384": { "cluster": 0, "id": 16384, "name": "swBuildId", "type": "string", "mandatory": false, "read": true, "write": false, "specific": false, "unit": null, default:'' }, 140 | 141 | "65281": { "cluster": 0, "id": 0xff01, "name": "xiaomiCustom1", "type": "string", "mandatory": false, "read": true, "write": false, "specific": true, "unit": null, default:'' }, 142 | }, 143 | "commands": { 144 | "0": { "id": 0, "name": "resetFactDefault" } 145 | }, 146 | "responses": { 147 | }, 148 | } 149 | -------------------------------------------------------------------------------- /src/coordinator/deviceTypes.js: -------------------------------------------------------------------------------- 1 | const Sym = require('./symbols.js'); 2 | var Path = require("path"); 3 | var Fs = require("fs"); 4 | 5 | class DeviceTypes { 6 | constructor(coordinator, options) { 7 | this[Sym.COORDINATOR] = coordinator; 8 | this[Sym.LOG] = (coordinator.log.getLogger && coordinator.log.getLogger('devicetypes') || coordinator.log); 9 | this.devices = { 'default': DeviceTypes.Default}; 10 | this.typesPath = options.devicetypes || Path.join(__dirname, '../../devices'); 11 | this.types = this.loadTypesDefinitions(this.typesPath); 12 | this.attributesForDeviceIdentification = [ 13 | {cluster:0x0000, attribute: 0x0003, name:'hwVersion'}, 14 | {cluster:0x0000, attribute: 0x0004, name:'manufacturerName'}, 15 | {cluster:0x0000, attribute: 0x0005, name:'modelId'}, 16 | {cluster:0x0000, attribute: 0x0006, name:'dateCode'}, 17 | {cluster:0x0000, attribute: 0x4000, name:'swBuildId'}, 18 | ]; 19 | 20 | this.coordCallbacks = { 21 | started: () => {}, 22 | stopped: () => {}, 23 | reset: () => {}, 24 | device_add: (device) => { this.onDeviceAdded(device); }, 25 | device_remove: (device) => { this.onDeviceRemoved(device); }, 26 | endpoint_add: (endpoint) => { this.onEndpointAdded(endpoint); }, 27 | cluster_add: (cluster) => { this.onClusterAdded(cluster); }, 28 | attribute_add: (attribute) => { this.onAttributeAdded(attribute); }, 29 | command_add: (command) => { this.onCommandAdded(command); }, 30 | attribute_change: (attribute, newval, oldval) => { this.onAttributeChanged(attribute, newval, oldval); }, 31 | } 32 | Object.entries(this.coordCallbacks).forEach(([name, fn]) => this[Sym.COORDINATOR].on(name, fn)); 33 | } 34 | 35 | type(id) { return this.types[id]; } 36 | get log() { return this[Sym.LOG]; } 37 | isAttributeForDeviceIdentification(clusterId, attributeId) { return this.attributesForDeviceIdentification.find(e => e.cluster === clusterId && e.attribute === attributeId); } 38 | loadTypesDefinitions(path) { 39 | let types = {}; 40 | let filenames = Fs.readdirSync(path).sort(); 41 | if (!filenames.length) throw new Error("error while loading profiles in '"+path+"'."); 42 | 43 | filenames.forEach((filename) => { 44 | try { 45 | let typedef = require(Path.resolve(path, filename)); 46 | typedef.id = typedef.id || Path.basename(filename, '.js'); 47 | typedef.name = typedef.name || filename; 48 | typedef.toString = function() { return '[type_'+this.id+']'; }; 49 | types[typedef.id] = typedef; 50 | } 51 | catch(e) { 52 | this.log.error("error while loading device type '"+filename+"':",e); 53 | } 54 | }); 55 | this.log.debug("finished loading "+Object.keys(types).length+" types from '"+path+"': "+(Object.keys(types).join(', '))); 56 | return types; 57 | } 58 | onDeviceAdded(device) { 59 | this.log.trace("catched device add "+device+""); 60 | this.getBestDeviceType(device); 61 | this.devices[device.address] = device; 62 | } 63 | onDeviceRemoved(device) { 64 | this.removeTypeFromDevice(device); 65 | delete this.devices[device.address]; 66 | } 67 | onEndpointAdded(endpoint) { 68 | let device = endpoint.device; 69 | let type = device[Sym.TYPE]; 70 | if (type && type['endpoint_add']) type['endpoint_add'](endpoint); 71 | } 72 | onClusterAdded(cluster) { 73 | let device = cluster.device; 74 | let type = device[Sym.TYPE]; 75 | if (type && type['cluster_add']) type['cluster_add'](cluster); 76 | } 77 | onAttributeAdded(attribute) { 78 | let device = attribute.device; 79 | let type = device[Sym.TYPE]; 80 | if (type && type['attribute_add']) type['attribute_add'](attribute); 81 | } 82 | onCommandAdded(command) { 83 | let device = command.device; 84 | let type = device[Sym.TYPE]; 85 | if (type && type['command_add']) type['command_add'](command); 86 | } 87 | onAttributeChanged(attribute, newval, oldval) { 88 | let device = attribute.device; 89 | let type = device[Sym.TYPE]; 90 | if (type && type['attribute_change']) type['attribute_change'](attribute, newval, oldval); 91 | } 92 | getBestDeviceType(device) { 93 | let besttype = DeviceTypes.Default 94 | let bestscore = besttype.match(device); 95 | 96 | Object.values(this.types).forEach((typedef) => { 97 | let score = typedef.match(device); 98 | if (score && score > bestscore) { 99 | besttype = typedef; 100 | bestscore = score; 101 | } 102 | }); 103 | this.log.trace("getBestDeviceType("+device+") matched type '"+besttype+"'."); 104 | return besttype; 105 | } 106 | 107 | removeTypeFromDevice(device) { 108 | let typedef = device[Sym.TYPE]; 109 | if (typedef) { 110 | if (typedef['type_remove']) typedef['type_remove'](device); 111 | 112 | // remove all values 113 | Object.keys(typedef.values || {}).forEach( (id) => { device.removeValue(id); } ); 114 | 115 | // remove all actions 116 | Object.keys(typedef.actions || {}).forEach( (id) => { device.removeAction(id); } ); 117 | 118 | device[Sym.TYPE] = null; 119 | this.log.trace("type "+typedef+"' removed from "+device); 120 | } 121 | } 122 | 123 | assignTypeToDevice(newtype, device) { 124 | let oldtype = device[Sym.TYPE]; 125 | if ( (oldtype && oldtype.id) !== (newtype && newtype.id) ) { 126 | 127 | if (oldtype) { 128 | this.removeTypeFromDevice(device); 129 | } 130 | 131 | if (newtype) { 132 | this.log.debug("setting up type "+newtype+" to "+device); 133 | device[Sym.TYPE] = newtype; 134 | 135 | // add all values 136 | Object.entries(newtype.values || {}).forEach( ([id, def]) => { 137 | device.addValue(id, def); 138 | }); 139 | 140 | // add all actions 141 | Object.entries(newtype.actions || {}).forEach( ([id, def]) => { 142 | device.addAction(id, def); 143 | }); 144 | 145 | // add all events 146 | Object.entries(newtype.events || {}).forEach( ([id, def]) => { 147 | device.addEvent(id, def); 148 | }); 149 | 150 | if (newtype['type_add']) newtype['type_add'](device); 151 | } 152 | } 153 | 154 | } 155 | 156 | toString() { return "[DeviceTypes-"+Object.keys(this.types).length+"]"; } 157 | } 158 | 159 | DeviceTypes.Default = { 160 | id: "default", 161 | match: function(device) { 162 | return -1; 163 | }, 164 | toString: function() { return "[type_"+this.id+"]"; } 165 | 166 | } 167 | 168 | module.exports = DeviceTypes; 169 | -------------------------------------------------------------------------------- /src/driver/responses/attribute_report.js: -------------------------------------------------------------------------------- 1 | const Enum = require('../enum.js'); 2 | 3 | // parsed response example: 4 | // attribute_report(0x8102), sequence:128, address:0x3dad, endpoint:1, 5 | // cluster:genBasic(0x0), attribute:65281, definition:unknown, 6 | // status:{"id":0,"name":"success","description":"Command was successful"}, 7 | // valuetype:string(0x42), value:"toto", rssi:108 8 | 9 | /* 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Zi.driver attribute_report(0x8102), sequence:90, address:0x7f69, endpoint:1, cluster:msRelativeHumidity(0x405), attribute:0, valuetype:uint16(0x21), value:252, rssi:201 23 | Zi.driver 01-81 02-00 0f-cf-5a-7f 69-01-04 05-00 00-00-21-00 02-19 fc-c9 24 | Zi - [device_0x7f69][cluster_msRelativeHumidity(0x405)][attr_measuredValue(0x0000)] data changed(252) 25 | 26 | Zi.driver attribute_report(0x8102), sequence:91, address:0x7f69, endpoint:1, cluster:msPressureMeasurement(0x403), attribute:0, valuetype:int16(0x29), value:229, rssi:204 27 | Zi.driver 01 81 02 00 0f c5 5b 7f 69 01 04 03 00 00 00 29 00 02 03 e6-cc 28 | Zi - [device_0x7f69][cluster_msPressureMeasurement(0x403)][attr_measuredValue(0x0000)] data changed(229) 29 | 30 | Zi.driver attribute_report(0x8102), sequence:91, address:0x7f69, endpoint:1, cluster:msPressureMeasurement(0x403), attribute:20, valuetype:int8(0x28), value:254, rssi:204 31 | Zi.driver 01 81 02 00 0e c8 5b 7f 69 01 04 03 00 14 00 28 00 01 ff-cc 32 | Zi - [device_0x7f69][cluster_msPressureMeasurement(0x403)][attr_(0x0014)] data changed(254) 33 | 34 | Zi.driver attribute_report(0x8102), sequence:91, address:0x7f69, endpoint:1, cluster:msPressureMeasurement(0x403), attribute:16, valuetype:int16(0x29), value:254, rssi:204 35 | Zi.driver 01 81 02 00 0f e9 5b 7f 69 01 04 03 00 10 00 29 00 02 26 ff-cc 36 | Zi - [device_0x7f69][cluster_msPressureMeasurement(0x403)][attr_(0x0010)] data changed(254) 37 | 38 | Zi.driver attribute_report(0x8102), sequence:92, address:0x7f69, endpoint:1, cluster:msTemperatureMeasurement(0x402), attribute:0, valuetype:int16(0x29), value:181, rssi:240 39 | Zi.driver 01 81 02 00 0f a6 5c 7f 69 01 04 02 00 00 00 29 00 02 0a b6-f0 40 | Zi - [device_0x7f69][cluster_msTemperatureMeasurement(0x402)][attr_measuredValue(0x0000)] data changed(181) 41 | 42 | Zi.driver attribute_report(0x8102), sequence:93, address:0x7f69, endpoint:1, cluster:msRelativeHumidity(0x405), attribute:0, valuetype:uint16(0x21), value:94, rssi:237 43 | Zi.driver 01 81 02 00 0f 43 5d 7f 69 01 04 05 00 00 00 21 00 02 14 5e-ed 44 | Zi - [device_0x7f69][cluster_msRelativeHumidity(0x405)][attr_measuredValue(0x0000)] data changed(94) 45 | 46 | Zi.driver attribute_report(0x8102), sequence:94, address:0x7f69, endpoint:1, cluster:msPressureMeasurement(0x403), attribute:0, valuetype:int16(0x29), value:229, rssi:237 47 | Zi.driver 01 81 02 00 0f e1 5e 7f 69 01 04 03 00 00 00 29 00 02 03 e6-ed 48 | Zi - [device_0x7f69][cluster_msPressureMeasurement(0x403)][attr_measuredValue(0x0000)] data changed(229) 49 | 50 | Zi.driver attribute_report(0x8102), sequence:94, address:0x7f69, endpoint:1, cluster:msPressureMeasurement(0x403), attribute:20, valuetype:int8(0x28), value:254, rssi:237 51 | Zi.driver 01 81 02 00 0e ec 5e 7f 69 01 04 03 00 14 00 28 00 01 ff-ed 52 | Zi - [device_0x7f69][cluster_msPressureMeasurement(0x403)][attr_(0x0014)] data changed(254) 53 | 54 | Zi.driver attribute_report(0x8102), sequence:94, address:0x7f69, endpoint:1, cluster:msPressureMeasurement(0x403), attribute:16, valuetype:int16(0x29), value:0, rssi:237 55 | Zi.driver 01 81 02 00 0f 33 5e 7f 69 01 04 03 00 10 00 29 00 02 27 00-ed 56 | Zi - [device_0x7f69][cluster_msPressureMeasurement(0x403)][attr_(0x0010)] data changed(0) 57 | 58 | */ 59 | 60 | 61 | 62 | module.exports = { 63 | id: 0x8102, 64 | name: "attribute_report", 65 | parse: function(reader, rep, version) { 66 | rep.sequence = reader.nextUInt8(); 67 | rep.address = reader.nextUInt16BE(); 68 | rep.endpoint = reader.nextUInt8(); 69 | rep.cluster = Enum.CLUSTERS(reader.nextUInt16BE()); 70 | rep.attribute = reader.nextUInt16BE(); 71 | rep.definition = (rep.cluster && rep.cluster.attributes && rep.cluster.attributes[rep.attribute]) || null; 72 | rep.status = Enum.COMMAND_STATUS(reader.nextUInt8()); 73 | rep.valuetype = Enum.ATTRIBUTE_TYPE(reader.nextUInt8(), new Error('unknown attribute type ')); 74 | rep.value = undefined; 75 | 76 | 77 | 78 | var attributeSize = reader.nextUInt16BE(); 79 | rep.valueData = reader.nextBuffer(attributeSize); 80 | rep.raw = JSON.stringify([...rep.valueData].map(e => Number(e).toString(16))); 81 | switch (rep.valuetype.id) { 82 | case 0x00: // null 83 | rep.value = null; 84 | break; 85 | 86 | case 0x10: // boolean 87 | rep.value = rep.valueData.readUIntBE(0, rep.valueData.length) ? true : false; 88 | break; 89 | 90 | case 0x18: // bitmap8 91 | case 0x19: // bitmap16 92 | case 0x1A: // bitmap24 93 | case 0x1B: // bitmap32 94 | case 0x1C: // bitmap40 95 | case 0x1D: // bitmap48 96 | case 0x1E: // bitmap56 97 | case 0x1F: // bitmap64 98 | rep.value = rep.valueData.readUIntBE(0, rep.valueData.length); 99 | break; 100 | 101 | case 0x20: // uint8 102 | case 0x21: // uint16 103 | case 0x22: // uint24 104 | case 0x23: // uint32 105 | case 0x24: // uint40 106 | case 0x25: // uint48 107 | case 0x26: // uint56 108 | case 0x27: // uint64 109 | rep.value = rep.valueData.readUIntBE(0, rep.valueData.length); 110 | break; 111 | 112 | case 0x28: // int8 113 | case 0x29: // int16 114 | case 0x2a: // int32 115 | rep.value = rep.valueData.readIntBE(0, rep.valueData.length); 116 | break; 117 | 118 | case 0x30: // enum 119 | var value = rep.valueData.readUIntBE(0, rep.valueData.length); 120 | if (rep.definition && rep.definition.enum && rep.definition.enum[value]) { 121 | rep.value = rep.definition.enum[rep.value]; 122 | } 123 | else { 124 | rep.value = { id: value, name:null }; 125 | } 126 | break; 127 | 128 | case 0x38: // semi precision (float) 129 | // TODO: test this. 130 | rep.value = float16_to_float( rep.valueData.readUInt16BE() ); 131 | break; 132 | 133 | case 0x39: // single precision 134 | rep.value = rep.valueData.readFloatBE(); 135 | break; 136 | 137 | case 0x3a: // double plecision 138 | rep.value = rep.valueData.readDoubleBE(); 139 | break; 140 | 141 | case 0x41: // byte string ==> TODO: should we keep it as raw buffer ??? 142 | case 0x42: // string 143 | rep.value = rep.valueData.toString('binary'); 144 | break; 145 | 146 | case 0xff: // unknown 147 | rep.value = rep.valueData; 148 | break; 149 | 150 | default: 151 | throw new Error("attribute_report: un-implemented read value type "+rep.valuetype+" (size="+attributeSize+")"); 152 | } 153 | }, 154 | }; 155 | 156 | 157 | 158 | const float16_to_float = function(h) { 159 | var s = (h & 0x8000) >> 15; 160 | var e = (h & 0x7C00) >> 10; 161 | var f = h & 0x03FF; 162 | 163 | if(e == 0) { 164 | return (s?-1:1) * Math.pow(2,-14) * (f/Math.pow(2, 10)); 165 | } else if (e == 0x1F) { 166 | return f?NaN:((s?-1:1)*Infinity); 167 | } 168 | 169 | return (s?-1:1) * Math.pow(2, e-15) * (1+(f/Math.pow(2, 10))); 170 | } 171 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/coordinator/deviceLoadSave.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const Fs = require('fs'); 3 | const Path = require('path'); 4 | const MkDirP = require('mkdirp'); 5 | 6 | const Sym = require('./symbols.js'); 7 | 8 | const Driver = require('../driver/driver.js'); 9 | const Device = require('./device.js'); 10 | const DeviceTypes = require('./deviceTypes.js'); 11 | 12 | const ZiEndpoint = require('./ziendpoint.js'); 13 | const ZiCluster = require('./zicluster.js'); 14 | const ZiAttribute = require('./ziattribute.js'); 15 | const ZiCommand = require('./zicommand.js'); 16 | 17 | const LOAD_IN_PROGRESS = Symbol('LOAD_IN_PROGRESS'); 18 | const FILE_DATA = Symbol('FILE_DATA'); 19 | const COORDINATOR_CALLBACKS = Symbol('COORDINATOR_CALLBACKS'); 20 | 21 | const LOAD_SAVE_LOGGERS = { 22 | nolog: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: ()=>{}, }, 23 | console: { trace: console.debug, debug: console.debug, info: console.log, warn: console.warn, error: console.error }, 24 | warn: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: console.warn, error: console.error }, 25 | error: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: console.error }, 26 | }; 27 | 28 | class DeviceLoadSave { 29 | constructor(coordinator, options) { 30 | options = options || { 31 | log: null, // 'nolog', 32 | loadsavepath: null, // './zigate_data.json' 33 | }; 34 | 35 | this[Sym.COORDINATOR] = coordinator || (() => { throw new Error("missing parameter 'coordinator'"); })(); 36 | this.path = options.path ||options.loadsavepath; 37 | this[Sym.LOG] = (typeof(options.log) === 'object' && options.log) 38 | || LOAD_SAVE_LOGGERS[options.log] 39 | || (coordinator.log.getLogger && coordinator.log.getLogger('loadsave')) 40 | || coordinator.log 41 | || LOAD_SAVE_LOGGERS['nolog']; 42 | this[LOAD_IN_PROGRESS] = false; 43 | this[FILE_DATA] = {}; 44 | this[COORDINATOR_CALLBACKS] = { 45 | error: (err) => { }, 46 | started: () => { this.onStart(); }, 47 | stopped: () => { }, 48 | reset: () => { }, 49 | 50 | device_add: (device) => { this.onDeviceAdded(device); }, 51 | device_remove: (device) => { this.onDeviceRemoved(device); }, 52 | endpoint_add: (endpoint) => { this.onEndpointAdded(endpoint); } , 53 | cluster_add: (cluster) => { this.onClusterAdded(cluster); }, 54 | attribute_add: (attribute) => { this.onAttributeAdded(attribute); }, 55 | attribute_change: (attribute, value, oldval) => { this.onAttributeChanged(attribute, value, oldval); }, 56 | }; 57 | Object.entries(this[COORDINATOR_CALLBACKS]).forEach( ([name, fn]) => this[Sym.COORDINATOR].on(name, fn) ); 58 | } 59 | 60 | static get LOGGERS() { return LOAD_SAVE_LOGGERS; }; 61 | 62 | shutdown() { 63 | Object.entries(this[COORDINATOR_CALLBACKS]).forEach( ([name, fn]) => this[Sym.COORDINATOR].removeListener(name, fn) ); 64 | } 65 | 66 | loadFile() { 67 | try { 68 | this.log.trace("loadFile("+this.path+")..."); 69 | this[LOAD_IN_PROGRESS] = true; 70 | if (!Fs.existsSync(this.path)) { 71 | this.log.info("initial zigate persistence file '"+this.path+"' doesn't exist ; creating a new one."); 72 | this.saveFile(); 73 | } 74 | this[FILE_DATA] = JSON.parse( Fs.readFileSync(this.path) ); 75 | 76 | // devices 77 | this[FILE_DATA].forEach(devicedata => { 78 | let device = this[Sym.COORDINATOR].addDevice(devicedata.address, devicedata.ieee); 79 | // endpoints 80 | devicedata.endpoints.filter(o => o.verified).forEach(endpointdata => { 81 | let endpoint = device.addEndpoint(endpointdata.id, endpointdata.verified); 82 | 83 | // clusters 84 | endpointdata.clusters.filter(o => o.verified).forEach(clusterdata => { 85 | let cluster = endpoint.addCluster(clusterdata.id, clusterdata.verified); 86 | 87 | // attributes 88 | clusterdata.attributes.filter(o => o.verified).forEach(attributedata => { 89 | cluster.addAttribute(attributedata.id, attributedata.value, attributedata.verified); 90 | }); 91 | 92 | // commands 93 | clusterdata.commands.filter(o => o.verified).forEach(commanddata => { 94 | cluster.addCommand(command.id, command.verified); 95 | }); 96 | }); // endOf-clusters 97 | }); // endOf-endpoints 98 | 99 | let deviceTypes = this[Sym.COORDINATOR].deviceTypes; 100 | let type = deviceTypes.type(devicedata.type) || deviceTypes.getBestDeviceType(device); 101 | this[Sym.COORDINATOR].setDeviceType(device, type); 102 | }); // endOf-devices 103 | 104 | this.log.info("successfully loaded persisted devices data from '"+this.path+"'."); 105 | 106 | } catch (e) { 107 | this.log.warn("zigate data file load error:",e); 108 | this[LOAD_IN_PROGRESS] = false; 109 | throw new Error("zigate data file load error: "+e); 110 | 111 | } finally { 112 | this[LOAD_IN_PROGRESS] = false; 113 | } 114 | } 115 | 116 | saveFile() { 117 | try{ 118 | this.log.trace("saveFile("+this.path+")..."); 119 | if (!Fs.existsSync(this.path)) { 120 | MkDirP.sync(Path.dirname(this.path)); 121 | } 122 | 123 | let deviceTypes = this[Sym.COORDINATOR].deviceTypes; 124 | 125 | // devices 126 | this[FILE_DATA] = this[Sym.COORDINATOR].devices.map(device => ({ 127 | address: device.address, 128 | hex: "0x"+(("0000"+Number(device.address).toString(16)).substr(-4,4)), 129 | ieee: device.ieee, 130 | type: device.type, 131 | 132 | // endpoints 133 | endpoints: device.endpoints.map(endpoint => ({ 134 | id: endpoint.id, 135 | hex: "0x"+(("0000"+Number(endpoint.id).toString(16)).substr(-4,4)), 136 | verified: endpoint.verified, 137 | // clusters 138 | clusters: endpoint.clusters.map(cluster => ({ 139 | id: cluster.id, 140 | hex: "0x"+(("0000"+Number(cluster.id).toString(16)).substr(-4,4)), 141 | name: (cluster.type && cluster.type.name) || undefined, 142 | verified: cluster.verified, 143 | // attributes 144 | attributes: cluster.attributes.map(attribute => ({ 145 | id: attribute.id, 146 | hex: "0x"+(("0000"+Number(attribute.id).toString(16)).substr(-4,4)), 147 | name: (attribute.type && cluster.type.name), 148 | value: deviceTypes.isAttributeForDeviceIdentification(cluster.id, attribute.id) ? attribute.value : undefined, 149 | verified: attribute.verified, 150 | })), 151 | // commands 152 | commands: cluster.commands.map(command => ({ 153 | id: command.id, 154 | hex: "0x"+(("0000"+Number(command.id).toString(16)).substr(-4,4)), 155 | name: (command.type && command.type.name), 156 | verified: command.verified, 157 | })), // commands 158 | 159 | })), // clusters 160 | 161 | })), // endpoints 162 | 163 | })); // devices 164 | 165 | Fs.writeFileSync(this.path, JSON.stringify(this[FILE_DATA], /*pretty print*/ null, 2 /*pretty print*/)); 166 | this.log.info("zigate data file saved in '"+this.path+"'."); 167 | } 168 | catch (e) { 169 | this.log.warn("zigate data file save error ("+this.path+"):",e); 170 | throw e; 171 | } 172 | } 173 | 174 | onStart() { 175 | this.loadFile(); 176 | } 177 | onStop() { 178 | this.saveFile(); 179 | } 180 | 181 | onDeviceAdded(device) { 182 | if (this[LOAD_IN_PROGRESS]) return; // skip 183 | 184 | let devicedata = this[FILE_DATA].find(d => d.address === device.address); 185 | if (devicedata) return; // device already present ; skip. 186 | 187 | this.saveFile(); 188 | } 189 | onDeviceRemoved(device) { 190 | if (this[LOAD_IN_PROGRESS]) return; // skip 191 | 192 | let devicedata = this[FILE_DATA].find(d => d.address === device.address); 193 | if (!devicedata) return; // device already removed ; skip. 194 | 195 | this.saveFile(); 196 | } 197 | onEndpointAdded(endpoint) { 198 | if (this[LOAD_IN_PROGRESS]) return; // skip 199 | 200 | let devicedata = this[FILE_DATA].find(d => d.address === endpoint.device.address); 201 | let endpointdata = devicedata && devicedata.endpoints.find(e => e.id === endpoint.id); 202 | if (endpointdata) return; // endpoint already present ; skip. 203 | 204 | this.saveFile(); 205 | } 206 | onClusterAdded(cluster) { 207 | if (this[LOAD_IN_PROGRESS]) return; // skip 208 | 209 | let devicedata = this[FILE_DATA].find(d => d.address === cluster.device.address); 210 | let endpointdata = devicedata && devicedata.endpoints.find(e => e.id === cluster.endpoint.id); 211 | let clusterdata = endpointdata && endpointdata.clusters.find(c => c.id === cluster.id); 212 | if (clusterdata) return; // cluster already present ; skip. 213 | 214 | this.saveFile(); 215 | } 216 | onAttributeAdded(attribute) { 217 | if (this[LOAD_IN_PROGRESS]) return; // skip 218 | 219 | let devicedata = this[FILE_DATA].find(d => d.address === attribute.device.address); 220 | let endpointdata = devicedata && devicedata.endpoints.find(e => e.id === attribute.endpoint.id); 221 | let clusterdata = endpointdata && endpointdata.clusters.find(c => c.id === attribute.cluster.id); 222 | let attributedata = clusterdata && clusterdata.attributes.find(a => a.id === attribute.id); 223 | if (attributedata) return; // attributedata already present ; skip. 224 | 225 | this.saveFile(); 226 | } 227 | 228 | onAttributeChanged(attribute, value, oldval) { 229 | if (this[LOAD_IN_PROGRESS]) return; // skip 230 | if (! (this[Sym.COORDINATOR].deviceTypes.isAttributeForDeviceIdentification(attribute.cluster.id, attribute.id))) return; 231 | 232 | let devicedata = this[FILE_DATA].find(d => d.address === attribute.device.address); 233 | let endpointdata = devicedata && devicedata.endpoints.find(e => e.id === attribute.endpoint.id); 234 | let clusterdata = endpointdata && endpointdata.clusters.find(c => c.id === attribute.cluster.id); 235 | let attributedata = clusterdata && clusterdata.attributes.find(a => a.id === attribute.id); 236 | if (attributedata.value === value) return; 237 | 238 | this.saveFile(); 239 | } 240 | 241 | get log() { return this[Sym.LOG]; } 242 | toString() { return "[DeviceLoadSave]"; } 243 | } 244 | 245 | module.exports = DeviceLoadSave; 246 | -------------------------------------------------------------------------------- /src/driver/driver.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const SerialPort = require('serialport'); 3 | const Delimiter = require('@serialport/parser-delimiter') 4 | const CommandBuilder = require('./commandBuilder.js'); 5 | const ResponseBuilder = require('./responseBuilder.js'); 6 | const Enum = require('./enum.js'); 7 | const util = require('util'); 8 | 9 | const FRAME_START = 0x01; 10 | const FRAME_STOP = 0x03; 11 | const FRAME_ESCAPE_XOR = 0x10; 12 | const FRAME_ESCAPE= 0x02; 13 | 14 | const DRIVER_LOGGERS = { 15 | nolog: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: ()=>{}, }, 16 | console: { trace: console.debug, debug: console.debug, info: console.log, warn: console.warn, error: console.error }, 17 | trace: { trace: console.debug, debug: console.debug, info: console.log, warn: console.warn, error: console.error }, 18 | debug: { trace: ()=>{}, debug: console.debug, info: console.log, warn: console.warn, error: console.error }, 19 | info: { trace: ()=>{}, debug: ()=>{}, info: console.log, warn: console.warn, error: console.error }, 20 | warn: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: console.warn, error: console.error }, 21 | error: { trace: ()=>{}, debug: ()=>{}, info: ()=>{}, warn: ()=>{}, error: console.error }, 22 | }; 23 | 24 | /* =========================== Driver events =================================== 25 | 'open', 'close', 'error', 26 | 'raw_out', 'command', 'command_xxx_yyy', 27 | 'raw_in', 'response', 'response_xxx_yyy' 28 | */ 29 | 30 | /* =========================== ONE FRAME ========================================= 31 | | 0 | 1 | 2 | 3 | 4 | 5 | n+5 | n+6 | n+7 | 32 | |-------|---------------|---------------|-------|--------------|-------|-------| 33 | | Start | MSG TYPE | LENGTH | CHKSM | DATA | RSSI | STOP | 34 | |-------|---------------|---------------|-------|--------------|-------|-------| 35 | | 0x01 | | n | | | | 0x03 | 36 | 37 | for every byte between 0x00 and 0x10, it must be replaced by an escape sequence: [0x02, (0x10 XOR byte) ] 38 | example: 0x06 becomes [0x02, 0x10 ^ 0x06] <=> [0x02, 0x16] 39 | checksum = ( 0x00 XOR MSG_TYPE XOR LENGTH XOR RSSI XOR DATA ) 40 | */ 41 | 42 | class Driver extends EventEmitter { 43 | constructor(options) { 44 | options = options || { 45 | log: null, 46 | commandspath: null, 47 | responsespath: null, 48 | }; 49 | super(); 50 | 51 | this.serialOptions = { 52 | baudRate: options.baudRate || 115200, 53 | dataBits: options.dataBits || 8, 54 | parity: options.parity || 'none', /* one of ['none', 'even', 'mark', 'odd', 'space'] */ 55 | stopBits: options.stopBits || 1, /* one of [1,2] */ 56 | lock: options.lock === false ? false : true, 57 | }; 58 | 59 | this.pendingCommands = []; 60 | 61 | this.port = null; 62 | this.parser = null; 63 | this.serial = null; 64 | this.logger = DRIVER_LOGGERS[options.log] 65 | || (options.log && options.log.getLogger && options.log.getLogger('driver')) 66 | || (options.log && options.log.trace && options.log.debug && options.log.info && options.log.warn && options.log.error && options.log) 67 | || DRIVER_LOGGERS['nolog']; 68 | 69 | CommandBuilder.LOGS = this.logger; 70 | this.commands = new CommandBuilder(); 71 | this.commands.loadCommands(options.commandspath || __dirname+'/commands'); 72 | 73 | ResponseBuilder.LOGS = this.logger; 74 | this.responses = new ResponseBuilder(); 75 | this.responses.loadResponses(options.responsespath || __dirname+'/responses'); 76 | 77 | if (this.port) { 78 | this.open(options.port); 79 | } 80 | } 81 | static get LOGGERS() { 82 | return DRIVER_LOGGERS; 83 | }; 84 | get isOpen() { 85 | return this.serial ? true : false; 86 | } 87 | get pending() { 88 | return this.pendingCommands.slice(); 89 | }; 90 | get version() { 91 | return this.commands.version; 92 | } 93 | get firmware() { 94 | let major = ""+ ((this.commands.version / 256) | 0); 95 | let minor = ""+(this.commands.version%256).toString(16); 96 | if (minor.length === 1) minor = '0'+minor; 97 | return major+'.'+minor; 98 | } 99 | 100 | open(port, callback) { 101 | if (port === 'auto') port = null; 102 | 103 | callback = callback || (()=>{}); 104 | 105 | if (!this.isOpen && port) { 106 | // connect to the port provided 107 | this.port = port; 108 | var p = new Promise((resolve, reject)=> { 109 | this.serial = new SerialPort(port, this.serialOptions, (err) => { 110 | if (err) { 111 | this.serial = null; 112 | this.parser = null; 113 | this.port = null; 114 | this.logger.error("[Driver] Error while opening ZiGate port '"+port+"': "+err); 115 | let ziError = new Error("Error while opening ZiGate port '"+port+"': "+err); 116 | if (process.platform.indexOf("win") === 0 && (""+err).indexOf('File not found') >=0) { 117 | ziError = new Error("Error while opening ZiGate port '"+port+"': "+err+" ; aren't windows drivers missing ?" ); 118 | } 119 | this.emitError(ziError); 120 | reject(ziError) 121 | } else { 122 | this.logger.debug("[Driver] successfully connected ZiGate port '"+this.port+"' ; retriveing version."); 123 | this.send('version').then((rep) => { 124 | this.commands.version = rep.installer; 125 | this.responses.version = rep.installer; 126 | this.logger.info("[Driver] well connnected to Zigate stick (firmware "+this.firmware+")."); 127 | resolve(this); 128 | this.emit('open'); 129 | }) 130 | .catch(err => { 131 | let ziError = new Error("Error while getting firmware version: "+err ); 132 | this.logger.error("[Driver] "+ziError); 133 | this.emitError(ziError); 134 | reject(ziError); 135 | }); 136 | } 137 | }); 138 | this.parser = this.serial.pipe(new Delimiter({ delimiter: [FRAME_STOP] })); 139 | this.parser.on('data', (raw_in) => { this.onSerialData(raw_in); }); 140 | this.serial.on('error', (err) => { this.onSerialError(err) }); 141 | this.serial.on('close', () => { this.onSerialClosed(this.port); }); 142 | }); 143 | if (callback) { 144 | p.then(() => { callback(undefined, this); }, (err) => { callback(err, undefined); }); 145 | } 146 | return p; 147 | } 148 | else if (!this.isOpen && !port) { 149 | // no port provided ? use 1st port gathered by auto-detection. 150 | return Driver.guessZigatePorts().then( 151 | (ports) => { 152 | return this.open(ports[0].comName, callback); 153 | },(err) => { 154 | throw new Error("no port provided & auto-detection failed."); 155 | }); 156 | } 157 | else { 158 | // already open ? close first and reopen with new (/same) port. 159 | return this.close().then(this.open(port, callback)); 160 | } 161 | } 162 | 163 | close(callback) { 164 | var p = new Promise((resolve, reject) => { 165 | if (this.serial) { 166 | var serial = this.serial 167 | this.serial = null; 168 | this.parser = null; 169 | this.port = null; 170 | this.commands.version = null; 171 | this.responses.version = null; 172 | if (serial.isOpen) { 173 | serial.close((err) => { 174 | if (!err) { 175 | resolve(); 176 | } else { 177 | reject(err); 178 | } 179 | }); 180 | } 181 | } 182 | else { 183 | // was not opened yet ; immediatly resolve. 184 | resolve(); 185 | } 186 | }); 187 | p = p.then(callback, callback); 188 | return p; 189 | } 190 | 191 | parseFrame(raw_in) { 192 | try { 193 | var data = this.unescapeData(raw_in); 194 | data.str = data.toString('hex').replace(/../g, "$& "); 195 | this.logger.debug("[Driver] unescaped frame: "+data.str); 196 | 197 | if (data[0] !== FRAME_START) { 198 | this.emitError(new Error("[Driver] corrupted frame received: invalid 'frame_start' character: " + data.str), /*autoclose*/ false); 199 | return false; 200 | } 201 | if (data.length < 6) { 202 | this.emitError(new Error("[Driver] corrupted frame received: less than 8 bytes long ("+data.length+" bytes). ", data), /*autoclose*/ false); 203 | return false; 204 | } 205 | 206 | let typeid = data.readUInt16BE(1); 207 | let length = data.readUInt16BE(3); 208 | let checksum = data.readUInt16BE(5); 209 | let payload = data.slice(6, 6+length-1); // the rest of frame except rssi (1 byte) 210 | let rssi = data[data.length-1]; 211 | 212 | if (payload.length !== length - 1) { 213 | this.emitError(new Error("[Driver] corrupted frame received: inconsistent frame length attribute ("+length+") vs. payload length ("+payload.length+") + 1 (rssi). " + data.str), /*autoclose*/ false); 214 | return false; 215 | } 216 | 217 | var response = this.responses.parse(typeid, payload); 218 | if (response) { 219 | if (typeof(rssi) !== 'undefined') response.rssi = rssi; 220 | this.logger.debug("[Driver] response received: ", util.inspect(response, {breakLength: 10000})); 221 | //this.logger.debug("[Driver] response raw data: ", data.str); 222 | this.emit('response_'+response.type.name, response); 223 | this.emit('response', response); 224 | 225 | // special handling for 'status' response messages. 226 | if (typeid === 0x8000) { 227 | this.postProcessStatusResponse(response); 228 | } 229 | else { 230 | this.postProcessNonStatusResponse(response); 231 | } 232 | return true; 233 | } 234 | else { 235 | this.emitError(new Error("[Driver] unrecognized response received (type="+typeid.toString(16)+"): "+payload.toString('hex').replace(/../g, "$& ")), /*autoclose*/ false); 236 | return false; 237 | } 238 | } catch (e) { 239 | this.logger.error("[Driver] exception while parsing frame: "+ e); 240 | this.logger.error(e.stack); 241 | this.logger.error("[Driver] raw data: "+raw_in.toString('hex').replace(/../g, "$& ")); 242 | } 243 | } 244 | postProcessStatusResponse(status) { 245 | // try to match with a pending command 246 | for (var commandIndex=0; commandIndex> 8; 336 | checksum ^= command.type.id %256; 337 | checksum ^= command.payload.length >> 8; 338 | checksum ^= command.payload.length %256; 339 | var i = 5; 340 | for (const b of command.payload) { 341 | raw_out[i++] = b; 342 | checksum ^= b; 343 | } 344 | raw_out.writeUInt8(checksum, 4); 345 | this.logger.debug("[Driver] sending command: ", util.inspect(command, {breakLength: 10000})); 346 | this.logger.debug("[Driver] sending frame: 01 "+raw_out.toString('hex').replace(/../g, "$& ")+ "03"); 347 | 348 | raw_out = this.escapeData(raw_out); 349 | this.logger.debug("[Driver] sending raw frame: 01 "+raw_out.toString('hex').replace(/../g, "$& ")+"03"); 350 | 351 | this.serial.write([FRAME_START]); 352 | this.serial.write(raw_out); 353 | this.serial.write([FRAME_STOP]); 354 | 355 | // registering this pending command if further responses are expected 356 | if (command.type.statusExpected || command.type.responseExpected) { 357 | this.pendingCommands.push(command); 358 | } 359 | else { 360 | command.cmdPromiseResolve(command); 361 | } 362 | 363 | this.emit('raw_out', raw_out); 364 | this.emit('command', command); 365 | this.emit('command_'+command.type.name, command); 366 | 367 | } catch (e) { 368 | this.logger.warn("[Driver] exception while sending command "+command.type+": "+e); 369 | command.cmdPromiseReject(e); 370 | } 371 | finally { 372 | return command.cmdPromise; 373 | } 374 | } 375 | 376 | static guessZigatePorts(callback/*(err, ports)*/) { 377 | callback = callback || (()=>{}); 378 | 379 | //console.debug("[Driver] retrieving list of available serial devices"); 380 | return SerialPort.list().then(ports => { 381 | var ziports = ports.filter((p) => { return p.vendorId && p.vendorId.toLowerCase() === '067b' && p.productId === '2303'; }); 382 | 383 | // 2nd attempt: guess port for (new) zigate TTL module cp2102 384 | if (!ziports.length) { 385 | ziports = ports.filter((p) => { return p.vendorId && p.vendorId.toLowerCase() === '10c4' && p.productId === 'ea60'; }); 386 | } 387 | 388 | if (ziports.length) { 389 | resolve(ziports); 390 | if (callback) callback(undefined, ziports); 391 | } 392 | else { 393 | reject(new Error(""+ports.length+" ports detected and no ZiGate port found.")); 394 | if (callback) callback(new Error(""+ports.length+" ports detected and no ZiGate port found.")); 395 | } 396 | }) 397 | .catch(err => { 398 | reject(err); 399 | if (callback) callback(err); 400 | }); 401 | 402 | /* ZiGate port example: { manufacturer: 'Prolific Technology Inc.', 403 | comName: '/dev/ttyUSB0' 404 | vendorId: '067b', 405 | productId: '2303', 406 | serialNumber: undefined, 407 | pnpId: 'usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0', 408 | locationId: undefined, 409 | } */ 410 | } 411 | 412 | emitError(err, autoclose) { 413 | this.logger.error("[Driver] ERROR: ", ""+err); 414 | process.nextTick(this.emit.bind(this, 'error', err)); 415 | if (autoclose && this.isOpen) { 416 | this.serial.close(); 417 | } 418 | } 419 | onSerialData(raw_in) { 420 | this.logger.debug("[Driver] received raw frame: "+raw_in.toString('hex').replace(/../g, "$& ")); 421 | this.emit('raw_in', raw_in.slice(1)); 422 | this.parseFrame(raw_in); 423 | } 424 | onSerialClosed(port) { 425 | if (this.serial) { 426 | this.logger.info("[Driver] port '"+port+"' closed."); 427 | this.emit('close', port); 428 | this.serial = null; 429 | this.parser = null; 430 | this.port = null; 431 | } 432 | } 433 | onSerialError(err) { 434 | this.emitError(err, /*autoclose*/ false); 435 | } 436 | 437 | unescapeData(data) { 438 | var decodedLength = 0; 439 | var decodedData = Buffer.alloc(data.length); 440 | var i=0; 441 | while (i0) { 443 | this.logger.warn("[Driver] FRAME_START(0x01) byte found in the middle of the data (pos "+i+"): 1st message after a ZiGate reset ?"); 444 | this.logger.warn("[Driver] => raw data = "+data.toString('hex').replace(/../g, "$& ")); 445 | return this.unescapeData(data.slice(i)); 446 | } 447 | else if (data[i] === FRAME_ESCAPE) { 448 | if ((i+1)=2 && def.length <=3) { 46 | enumObj = { 47 | id: def[0], 48 | name: def[1], 49 | description: def[2] || undefined, 50 | }; 51 | } 52 | else if (def && typeof(def) === 'object' && !isNaN(parseInt(def.id)) && typeof(def.name) === 'string') { 53 | enumObj = def; 54 | } 55 | else { 56 | throw new Error("invalid enum object definition provided: "+JSON.stringify(def)); 57 | } 58 | if (byKeys.has(enumObj.id)) { 59 | throw new Error("cannot add new entry in enum '"+enumsetname+"': colliding identifier '"+enumObj.id+"'"); 60 | } 61 | else if (byNames.has(enumObj.name)) { 62 | throw new Error("cannot add new entry in enum '"+enumsetname+"': colliding name '"+enumObj.name+"'"); 63 | } 64 | 65 | enumObj.toString = /*enumObj.toString ||*/ (() => { return enumObj.name+ ('(0x'+enumObj.id.toString(16)+')').grey; }); 66 | enumObj[util.inspect.custom] = /*enumObj.inspect ||*/ ((depth, opts) => { return enumObj.toString(); }); 67 | byKeys.set(enumObj.id, enumObj); 68 | byNames.set(enumObj.name, enumObj); 69 | }; 70 | 71 | // end of enumset construction: register (optionally) provided definitions 72 | if (Array.isArray(definitions)) { 73 | definitions.forEach(enumSet.add); 74 | } 75 | else if (definitions && typeof(definitions) === 'object') { 76 | for (let n in definitions) { 77 | var e = definitions[n]; 78 | 79 | // array like [id, name, description] 80 | if (Array.isArray(e) && e.length >= 2 && !isNaN(parseInt(e[0])) && typeof(e[1]) === 'string') { 81 | enumSet.add(e); 82 | } 83 | 84 | // literal object like { 0: "name1", 1:"name2", ...} 85 | else if (typeof(e) === 'string' && !isNaN(parseInt(n))) { 86 | enumSet.add({id:n, name:e}); 87 | } 88 | // literal object like { "name1": int_1, "name2": int_2, ...} 89 | else if (!isNaN(parseInt(e)) && typeof(n) === 'string') { 90 | enumSet.add({id:e, name:n}); 91 | } 92 | else if (e && typeof(e) === 'object') { 93 | // list of objects with (id & name), or (name) only + key being an integer 94 | if (typeof(e.name) === 'string' && (!isNaN(parseInt(e.id)) || !isNaN(parseInt(n))) ) { 95 | e.id = (!isNaN(parseInt(e.id))) ? parseInt(e.id) : parseInt(n); 96 | enumSet.add(e); 97 | } 98 | // list of objects with (id) only + key being a string 99 | else if (e && typeof(e) === 'object' && typeof(n) === 'string' && !isNaN(parseInt(e.id))) { 100 | e.name = n; 101 | enumSet.add(e); 102 | } 103 | } 104 | } 105 | } 106 | 107 | Enum[enumsetname] = enumSet; 108 | return enumSet; 109 | }; 110 | 111 | 112 | Enum.create('CERTIFICATION', [ 113 | [1, "CE", ], 114 | [2, "FCC", ], 115 | ]); 116 | 117 | Enum.create('STATUS', [ 118 | [0, "success", ], 119 | [1, "invalid_params", ], 120 | [2, "unhandled_command", ], 121 | [3, "command_failed", ], 122 | [4, "busy", "Node is carrying out a lengthy operation and is currently unable to handle the incoming command"], 123 | [5, "stack_already started", "Stack already started (no new configuration accepted)"], 124 | ]); 125 | 126 | Enum.create('ZCL_STATUS', [ 127 | [ 0, 'success', ], 128 | [ 1, 'fail', ], 129 | [ 2, 'parameter_null', ], 130 | [ 3, 'parameter_range', ], 131 | [ 4, 'heap_fail', ], 132 | [ 5, 'ep_range', "endpoint number was out-of-range", ], 133 | [ 6, 'ep_unknown', "endpoint has not been registered with the ZCL (but endpoint number was in-range)", ], 134 | [ 7, 'security_range', ], 135 | [ 8, 'cluster_0', "Specified endpoint has no clusters", ], 136 | [ 9, 'cluster_null', ], 137 | [10, 'cluster_not_found', "cluster has not been registered with the ZCL", ], 138 | [11, 'cluster_id_range', ], 139 | [12, 'attributes_null', ], 140 | [13, 'attributes_0', "list of attributes to be read was empty", ], 141 | [14, 'attribute_wo', ], 142 | [15, 'attribute_ro', ], 143 | [16, 'attributes_access', ], 144 | [17, 'attribute_type_unsupported', ], 145 | [18, 'attribute_not_found', ], 146 | [19, 'callback_null', ], 147 | [20, 'zbuffer_fail', "No buffer available to transmit message", ], 148 | [21, 'ztransmit_fail', "ZigBee PRO stack has reported a transmission error", ], 149 | [22, 'client_server_status', ], 150 | [23, 'timer_resource', ], 151 | [24, 'attribute_is_client', "Attempt made by a cluster client to read a client attribute", ], 152 | [25, 'attribute_is_server', "Attempt made by a cluster server to read a server attribute ", ], 153 | [26, 'attribute_range', ], 154 | [27, 'attribute_mismatch', ], 155 | [28, 'key_establishment_more_than_one_cluster', "Cluster that requires application-level (APS) security has been accessed using a packet that has not been encrypted with the application link key", ], 156 | [29, 'insufficient_space', ], 157 | [30, 'no_reportable_change', ], 158 | [31, 'no_report_entries', ], 159 | [32, 'attribute_not_reportable', ], 160 | [33, 'attribute_id_order', ], 161 | [34, 'malformed_message', ], 162 | [35, 'manufacturer_specific', ], 163 | [36, 'profile_id', ], 164 | [37, 'invalid_value', ], 165 | [38, 'cert_not_found', ], 166 | [39, 'custom_data_null', ], 167 | [40, 'time_not_synchronised', ], 168 | [41, 'signature_verify_failed', ], 169 | [42, 'zreceive_fail', ], 170 | [43, 'key_establishment_end_point_not_found', ], 171 | [44, 'key_establishment_cluster_entry_not_found',], 172 | [45, 'key_establishment_callback_error', ], 173 | [46, 'security_insufficient_for_cluster', ], 174 | [47, 'custom_command_handler_null_or_returned_error',], 175 | [48, 'invalid_image_size', ], 176 | [49, 'invalid_image_version', ], 177 | [50, '_attr_req_not_finished', ], 178 | [51, '_attribute_access', ], 179 | [52, 'security_fail', ], 180 | [53, 'cluster_command_not_found', ], 181 | ]); 182 | 183 | Enum.create('ATTRIBUTE_TYPE', [ 184 | {id: 0x00, name: 'null', length:0, }, 185 | {id: 0x08, name: 'data8', length:1, }, 186 | {id: 0x09, name: 'data16', length:2, }, 187 | {id: 0x0A, name: 'data24', length:3, }, 188 | {id: 0x0B, name: 'data32', length:4, }, 189 | {id: 0x0C, name: 'data40', length:5, }, 190 | {id: 0x0D, name: 'data48', length:6, }, 191 | {id: 0x0E, name: 'data56', length:7, }, 192 | {id: 0x0F, name: 'data64', length:8, }, 193 | {id: 0x10, name: 'boolean', length:1, }, 194 | {id: 0x18, name: 'bitmap8', length:1, }, 195 | {id: 0x19, name: 'bitmap16', length:2, }, 196 | {id: 0x1A, name: 'bitmap24', length:3, }, 197 | {id: 0x1B, name: 'bitmap32', length:4, }, 198 | {id: 0x1C, name: 'bitmap40', length:5, }, 199 | {id: 0x1D, name: 'bitmap48', length:6, }, 200 | {id: 0x1E, name: 'bitmap56', length:7, }, 201 | {id: 0x1F, name: 'bitmap64', length:8, }, 202 | {id: 0x20, name: 'uint8', length:1, }, 203 | {id: 0x21, name: 'uint16', length:2, }, 204 | {id: 0x22, name: 'uint24', length:3, }, 205 | {id: 0x23, name: 'uint32', length:4, }, 206 | {id: 0x24, name: 'uint40', length:5, }, 207 | {id: 0x25, name: 'uint48', length:6, }, 208 | {id: 0x26, name: 'uint56', length:7, }, 209 | {id: 0x27, name: 'uint64', length:8, }, 210 | {id: 0x28, name: 'int8', length:1, }, 211 | {id: 0x29, name: 'int16', length:2, }, 212 | {id: 0x2a, name: 'int24', length:3, }, 213 | {id: 0x2b, name: 'int32', length:4, }, 214 | {id: 0x2c, name: 'int40', length:5, }, 215 | {id: 0x2d, name: 'int48', length:6, }, 216 | {id: 0x2e, name: 'int56', length:7, }, 217 | {id: 0x2f, name: 'int64', length:8, }, 218 | {id: 0x30, name: 'enum8', length:1, }, 219 | {id: 0x31, name: 'enum16', length:2, }, 220 | {id: 0x38, name: 'semiPrec', length:2, }, 221 | {id: 0x39, name: 'singlePrec', length:4, }, 222 | {id: 0x3A, name: 'doublePrec', length:8, }, 223 | {id: 0x41, name: 'bstring', length:null, }, // octetStr 224 | {id: 0x42, name: 'string', length:null, }, // charStr 225 | {id: 0x43, name: 'lbstring', length:null, }, // longOctetStr 226 | {id: 0x44, name: 'lstring', length:null, }, // longCharStr 227 | {id: 0x48, name: 'array', length:null, }, 228 | {id: 0x4C, name: 'struct', length:null, }, 229 | {id: 0x50, name: 'set', length:null, }, 230 | {id: 0x51, name: 'bag', length:null, }, 231 | {id: 0xE0, name: 'time', length:4, }, // 1 byte: hour(00...23) ; 1 byte: minutes(00...59) ; 1 byte: seconds(00...59) ; 1 byte: 100th(00...99) 232 | {id: 0xE1, name: 'date', length:4, }, // 1 byte: year(1900...2155) ; 1 byte: month(01...12) ; 1 byte: day(01...31) ; 1 byte: weekday(01(monday)...07(sunday) ) ; 233 | {id: 0xE2, name: 'utc', length:4, }, // unsigned 32bit number of seconds since 1st of January, 2000 00:00:00 UTC 234 | {id: 0xE8, name: 'cluster', length:2, }, 235 | {id: 0xE9, name: 'attribute', length:2, }, 236 | {id: 0xF0, name: 'ieee', length:8, }, 237 | {id: 0xF1, name: 'seckey', length:16, }, 238 | {id: 0xFF, name: 'unknown', length:0, }, 239 | ]); 240 | 241 | /* teZCL_CommandStatus */ 242 | Enum.create('COMMAND_STATUS', [ 243 | [0x00, 'success', 'Command was successful'], 244 | [0x01, 'failure', 'Command was unsuccessful'], 245 | [0x7e, 'not_authorized', 'Sender does not have authorisation to issue the command'], 246 | [0x7f, 'reserved_field_not_zero', 'A reserved field of command is not set to zero'], 247 | [0x80, 'malformed_command', 'Command has missing fields or invalid field values'], 248 | [0x81, 'unsup_cluster_command', 'The specified cluster has not been registered with the ZCL on the device'], 249 | [0x82, 'unsup_general_command', 'Command does not have a handler enabled in the zcl_options.h file'], 250 | [0x83, 'unsup_manuf_cluster_command', 'Manufacturer-specific cluster command is not supported or has unknown manufacturer code'], 251 | [0x84, 'unsup_manuf_general_command', 'Manufacturer-specific ZCL command is not supported or has unknown manufacturer code'], 252 | [0x85, 'invalid_field', 'Command has field which contains invalid value'], 253 | [0x86, 'unsupported_attribute', 'Specified attribute is not supported on the device'], 254 | [0x87, 'invalid_value', 'Specified attribute value is out of range or a reserved value'], 255 | [0x88, 'read_only', 'Attempt to write to read-only attribute'], 256 | [0x89, 'insufficient_space', 'Not enough memory space to perform requested operation'], 257 | [0x8A, 'duplicate_exists', 'Attempt made to create a table entry that already exists in the target table'], 258 | [0x8B, 'not_found', 'Requested information cannot be found'], 259 | [0x8C, 'unreportable_attribute', 'Periodic reports cannot be produced for this attribute'], 260 | [0x8D, 'invalid_data_type', 'Invalid data type specified for attribute'], 261 | [0x8E, 'invalid_selector', 'Incorrect selector for this attribute'], 262 | [0x8F, 'write_only', 'Issuer of command does not have authorisation to read specified attribute'], 263 | [0x90, 'inconsistent_startup_state', 'Setting the specified values would put device into an inconsistent state on start-up'], 264 | [0x91, 'defined_out_of_band', 'Attempt has been made to write to attribute using an out-of-band method or not over-air'], 265 | [0x92, 'inconsistent', ''], 266 | [0x93, 'action_denied', ''], 267 | [0x94, 'timeout', ''], 268 | [0xc0, 'hardware_failure', 'Command was unsuccessful due to hardware failure'], 269 | [0xc1, 'software_failure', 'Command was unsuccessful due to software failure'], 270 | [0xc2, 'calibration_error', ''], 271 | [0xc3, 'unsupported_cluster', ''], 272 | ]); 273 | 274 | Enum.create('READ_WRITE_ATTRIBUTE_STATUS', [ 275 | [0x07, 'bad_length', 'zero attributes request or bad count in frame fields'], 276 | [0x01, 'network_down', "Invalid Call, the network is down or the application is not joined to a network"], 277 | [0x02, 'invalid_data', "Tried to read more than 15 attributes"], 278 | [0x0C, 'not_enough_memory', 'Insufficient Memory, there is not enough available memory to transmit the message'], 279 | [0x00, 'success', 'the message was successfully transmitted'], 280 | [0xFF, 'fail_unknown', 'Unknown Failure'], 281 | ]); 282 | 283 | Enum.create('PROFILES', [ 284 | [0x0104, 'ha','ZigBee HA'], 285 | [0x0105, 'ba',''], 286 | [0x0107, 'hc',''], 287 | [0x0108, 'ts',''], 288 | [0x0109, 'se',''], 289 | [0x010A, 'rs',''], 290 | [0xC05E, 'll',''], 291 | ]); 292 | 293 | Enum.create('PERMIT_JOIN_STATUS', [ 294 | [1, 'on', 'devices are allowed to join network'], 295 | [0, 'off', 'devices are not allowed join the network'], 296 | ]); 297 | Enum.create('RESTART_STATUS', [ 298 | [0, 'startup', ''], 299 | [2, 'nfn_start', ''], 300 | [6, 'running', ''], 301 | ]); 302 | 303 | Enum.create('LOG_LEVEL', [ 304 | [0, "emergency", ''], 305 | [1, "alert", ''], 306 | [2, "critical", ''], 307 | [3, "error", ''], 308 | [4, "warning", ''], 309 | [5, "notice", ''], 310 | [6, "information", ''], 311 | [7, "debug", ''], 312 | ]); 313 | 314 | Enum.create('DIRECTION', [ 315 | [0, "srv_to_cli", 'server to client <=> read a value'], 316 | [1, "cli_to_srv", 'client to server <=> write a value'], 317 | ]); 318 | 319 | Enum.create('CERTIFICATION', [ 320 | [1, "CE", 'CE certification'], 321 | [2, "FCC", ' FCC certification'], 322 | ]); 323 | 324 | Enum.create('NETWORK_JOIN_STATUS', [ 325 | [0, 'joined_existing_network', ''], 326 | [1, 'formed_new_network', ''], 327 | ]); 328 | for (var i=128; i<=244; ++i) Enum.NETWORK_JOIN_STATUS.add([i, 'failed_'+i, 'network join failed (error 0x'+i.toString(16)+')']); 329 | 330 | Enum.create('NODE_LOGICAL_TYPE', [ 331 | [0x00, 'coordinator', ''], 332 | [0x01, 'router', ''], 333 | [0x02, 'end_device', ''], 334 | ]); 335 | 336 | Enum.create('ADDRESS_MODE', [ 337 | [ 0, 'bound', 'Use one or more bound nodes/endpoints, with acknowledgements'], 338 | [ 1, 'group', 'Use a pre-defined group address, with acknowledgements'], 339 | [ 2, 'short', 'Use a 16-bit network address, with acknowledgements'], 340 | [ 3, 'ieee', 'Use a 64-bit IEEE/MAC address, with acknowledgements'], 341 | [ 4, 'broadcast', 'Perform a broadcast'], 342 | [ 5, 'no_transmit', 'Do not transmit'], 343 | [ 6, 'bound_no_ack', 'Perform a bound transmission, with no acknowledgements'], 344 | [ 7, 'short_no_ack', 'Perform a transmission using a 16-bit network address, with no acknowledgements'], 345 | [ 8, 'ieee_no_ack', 'Perform a transmission using a 64-bit IEEE/MAC address, with no acknowledgements'], 346 | [ 9, 'bound_non_blocking', 'Perform a non-blocking bound transmission, with acknowledgements '], 347 | [10, 'bound_non_blocking_no_ack', 'Perform a non-blocking bound transmission, with no acknowledgements '], 348 | ]); 349 | 350 | Enum.create('DEVICE_TYPE', [ 351 | [ 0, 'coordinator' , ''], 352 | [ 1, 'router' , ''], 353 | [ 2, 'legacy_router' , ''], 354 | ]); 355 | 356 | Enum.create('DEVICE_HA_TYPE', [ 357 | [0x0000, 'On/Off Switch'], 358 | [0x0001, 'Level Control Switch'], 359 | [0x0002, 'On/Off Output'], 360 | [0x0003, 'Level Controllable Output'], 361 | [0x0004, 'Scene Selector'], 362 | [0x0005, 'Configuration Tool'], 363 | [0x0006, 'Remote Control'], 364 | [0x0007, 'Combined Interface'], 365 | [0x0008, 'Range Extender'], 366 | [0x0009, 'Mains Power Outlet'], 367 | [0x000A, 'Door Lock'], 368 | [0x000B, 'Door Lock Controller'], 369 | [0x000C, 'Simple Sensor'], 370 | [0x0100, 'On/Off Light'], 371 | [0x0101, 'Dimmable Light'], 372 | [0x0102, 'Color Dimmable Light'], 373 | [0x0103, 'On/Off Light Switch'], 374 | [0x0104, 'Dimmer Switch'], 375 | [0x0105, 'Color Dimmer Switch'], 376 | [0x0106, 'Light Sensor'], 377 | [0x0107, 'Occupancy Sensor'], 378 | [0x0200, 'Shade'], 379 | [0x0201, 'Shade Controller'], 380 | [0x0202, 'Window Covering Device'], 381 | [0x0203, 'Window Covering Controller'], 382 | [0x0300, 'Heating/Cooling Unit'], 383 | [0x0301, 'Thermostat'], 384 | [0x0302, 'Temperature Sensor'], 385 | [0x0303, 'Pump'], 386 | [0x0304, 'Pump Controller'], 387 | [0x0305, 'Pressure Sensor'], 388 | [0x0306, 'Flow Sensor'], 389 | [0x0400, 'IAS Control and Indicating Equipment'], 390 | [0x0401, 'IAS Ancillary Control Equipment'], 391 | [0x0402, 'IAS Zone'], 392 | [0x0403, 'IAS Warning Device'], 393 | [0x5f01, 'unknown'], 394 | 395 | ]); 396 | 397 | 398 | Enum.create('CLUSTERS', require('./clusters/clusterDefinitions.js')); 399 | 400 | 401 | module.exports = Enum; 402 | --------------------------------------------------------------------------------