diff --git a/lib/controller.js b/lib/controller.js index 61dc8e1b..3cbb82b8 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -230,18 +230,18 @@ class Controller { } async publishEntityState(IDorName, payload) { - const entity = settings.getEntity(IDorName); - if (!entity) { + const entity = await this.zigbee.resolveEntity(IDorName); + if (!entity || !entity.settings) { logger.error(`'${IDorName}' does not exist, skipping publish`); return; } let messagePayload = {...payload}; - const currentState = this.state.exists(entity.ID) ? this.state.get(entity.ID) : {}; + const currentState = this.state.exists(entity.settings.ID) ? this.state.get(entity.settings.ID) : {}; const newState = objectAssignDeep.noMutate(currentState, payload); // Update state cache with new state. - this.state.set(entity.ID, newState); + this.state.set(entity.settings.ID, newState); if (settings.get().advanced.cache_state) { // Add cached state to payload @@ -249,28 +249,32 @@ class Controller { } const options = { - retain: entity.hasOwnProperty('retain') ? entity.retain : false, - qos: entity.hasOwnProperty('qos') ? entity.qos : 0, + retain: entity.settings.hasOwnProperty('retain') ? entity.settings.retain : false, + qos: entity.settings.hasOwnProperty('qos') ? entity.settings.qos : 0, }; if (entity.type === 'device' && settings.get().mqtt.include_device_information) { - const device = await this.zigbee.getDevice({ieeeAddr: entity.ID}); + const device = await this.zigbee.getDevice({ieeeAddr: entity.device.ieeeAddr}); const attributes = [ 'ieeeAddr', 'networkAddress', 'type', 'manufacturerID', 'manufacturerName', 'powerSource', 'applicationVersion', 'stackVersion', 'zclVersion', 'hardwareVersion', 'dateCode', 'softwareBuildID', ]; - messagePayload.device = {friendlyName: entity.friendly_name}; + messagePayload.device = {friendlyName: entity.name}; attributes.forEach((a) => messagePayload.device[a] = device[a]); } + if (entity.type === 'device' && settings.get().advanced.last_seen !== 'disable') { + messagePayload.last_seen = utils.formatDate(entity.device.lastSeen, settings.get().advanced.last_seen); + } + if (Object.entries(messagePayload).length) { if (settings.get().experimental.output === 'json') { - await this.mqtt.publish(entity.friendly_name, JSON.stringify(messagePayload), options); + await this.mqtt.publish(entity.name, JSON.stringify(messagePayload), options); } else { /* istanbul ignore else */ if (settings.get().experimental.output === 'attribute') { - await this.iteratePayloadAttributeOutput(`${entity.friendly_name}/`, messagePayload, options); + await this.iteratePayloadAttributeOutput(`${entity.name}/`, messagePayload, options); } } } diff --git a/lib/extension/deviceReceive.js b/lib/extension/deviceReceive.js index 260e5c4f..364a022a 100644 --- a/lib/extension/deviceReceive.js +++ b/lib/extension/deviceReceive.js @@ -110,13 +110,8 @@ class DeviceReceive { payload.linkquality = data.linkquality; } - // Add last seen timestamp - const now = Date.now(); - if (settings.get().advanced.last_seen !== 'disable') { - payload.last_seen = utils.formatDate(now, settings.get().advanced.last_seen); - } - if (settings.get().advanced.elapsed) { + const now = Date.now(); if (this.elapsed[data.device.ieeeAddr]) { payload.elapsed = now - this.elapsed[data.device.ieeeAddr]; } diff --git a/lib/extension/entityPublish.js b/lib/extension/entityPublish.js index f6cb07e4..7d0b1c83 100644 --- a/lib/extension/entityPublish.js +++ b/lib/extension/entityPublish.js @@ -158,10 +158,6 @@ class EntityPublish { delete msg.state; } - if (settings.get().advanced.last_seen !== 'disable') { - msg.last_seen = utils.formatDate(Date.now(), settings.get().advanced.last_seen); - } - this.publishEntityState(entity.settings.ID, msg); } diff --git a/lib/extension/networkMap.js b/lib/extension/networkMap.js index 1645cd4a..de968e83 100644 --- a/lib/extension/networkMap.js +++ b/lib/extension/networkMap.js @@ -8,7 +8,6 @@ class NetworkMap { this.zigbee = zigbee; this.mqtt = mqtt; this.state = state; - this.lastSeen = new Map(); // Subscribe to topic. this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`; @@ -25,12 +24,6 @@ class NetworkMap { }; } - onZigbeeEvent(type, data, mappedDevice, settingsDevice) { - if (data.device) { - this.lastSeen.set(data.device.ieeeAddr, Date.now()); - } - } - onMQTTConnected() { this.mqtt.subscribe(this.topic); this.mqtt.subscribe(this.topicRoutes); @@ -80,7 +73,7 @@ class NetworkMap { // Add the device last_seen timestamp let lastSeen = 'unknown'; - const date = device.type === 'Coordinator' ? Date.now() : this.lastSeen.get(device.ieeeAddr); + const date = device.type === 'Coordinator' ? Date.now() : device.lastSeen; if (date) { const lastSeenAgo = `${new Date(Date.now() - date).toISOString().substr(11, 8)}s ago`; lastSeen = utils.formatDate(date, settings.get().advanced.last_seen, lastSeenAgo); @@ -166,7 +159,7 @@ class NetworkMap { networkMap.nodes.push({ ieeeAddr: device.ieeeAddr, friendlyName: resolved.name, type: device.type, networkAddress: device.networkAddress, manufacturerName: device.manufacturerName, - modelID: device.modelID, failed: failed.get(device), + modelID: device.modelID, failed: failed.get(device), lastSeen: device.lastSeen, }); } diff --git a/lib/state.js b/lib/state.js index 5d8b0822..76eff325 100644 --- a/lib/state.js +++ b/lib/state.js @@ -1,7 +1,6 @@ const logger = require('./util/logger'); const data = require('./util/data'); const fs = require('fs'); -const settings = require('./util/settings'); const objectAssignDeep = require('object-assign-deep'); const saveInterval = 1000 * 60 * 5; // 5 minutes @@ -18,8 +17,6 @@ class State { this.state = {}; this.file = data.joinPath('state.json'); this.timer = null; - - this.handleSettingsChanged = this.handleSettingsChanged.bind(this); } start() { @@ -28,27 +25,6 @@ class State { // Save the state on every interval this.clearTimer(); this.timer = setInterval(() => this.save(), saveInterval); - - // Listen for on settings changed events. - settings.addOnChangeHandler(this.handleSettingsChanged); - - this.checkLastSeen(); - } - - handleSettingsChanged() { - this.checkLastSeen(); - } - - checkLastSeen() { - if (settings.get().advanced.last_seen === 'disable') { - Object.values(this.state).forEach((s) => { - if (s.hasOwnProperty('last_seen')) { - delete s.last_seen; - } - }); - - this.save(); - } } clearTimer() { diff --git a/lib/util/settings.js b/lib/util/settings.js index 222845a4..ca45f54d 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -3,7 +3,6 @@ const file = data.joinPath('configuration.yaml'); const objectAssignDeep = require(`object-assign-deep`); const path = require('path'); const yaml = require('./yaml'); -const onChangeHandlers = []; const defaults = { whitelist: [], @@ -127,7 +126,6 @@ function write() { _settings = read(); _settingsWithDefaults = objectAssignDeep.noMutate(defaults, get()); - onChangeHandlers.forEach((handler) => handler()); } function read() { @@ -387,11 +385,6 @@ function changeFriendlyName(IDorName, newName) { write(); } -function addOnChangeHandler(handler) { - onChangeHandlers.push(handler); -} - - module.exports = { get: getWithDefaults, set, @@ -408,7 +401,6 @@ module.exports = { addDeviceToGroup, removeDeviceFromGroup, changeDeviceOptions, - addOnChangeHandler, changeFriendlyName, // For tests only diff --git a/lib/zigbee.js b/lib/zigbee.js index d1b30717..d36ba4a5 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -116,9 +116,16 @@ class Zigbee extends events.EventEmitter { } async resolveEntity(key) { - assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`); + assert( + typeof key === 'string' || typeof key === 'number' || + key.constructor.name === 'Device', `Wrong type '${typeof key}'` + ); + + if (typeof key === 'string' || typeof key === 'number') { + if (typeof key === 'number') { + key = key.toString(); + } - if (typeof key === 'string') { if (key === 'coordinator') { const coordinator = await this.getDevice({type: 'Coordinator'}); return { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b189c0ce..8a41e6c0 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -5938,9 +5938,9 @@ } }, "zigbee-herdsman": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.6.7.tgz", - "integrity": "sha512-DtgI9Gq5fvBtgW8EUT/Gb1hEtzesOyWSQExiS6fGQaDNq17/6Wy78NUkVkYLUTYRTz5QS9HTqWPEqQFv8qjbFg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.6.8.tgz", + "integrity": "sha512-S96Bq9UD1jnPe8G4lg5Rarq3ZiCw/ryFEeyvKtm6p5hN83yqRx/YdbDQNW2/+or1My0zbIfG/06MyjsoMtn/dQ==", "requires": { "debug": "^4.1.1", "fast-deep-equal": "^2.0.1", diff --git a/package.json b/package.json index 46590c27..81b04726 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "rimraf": "*", "semver": "*", "winston": "2.4.2", - "zigbee-herdsman": "0.6.7", + "zigbee-herdsman": "0.6.8", "zigbee-herdsman-converters": "11.0.9" }, "devDependencies": { diff --git a/test/deviceReceive.test.js b/test/deviceReceive.test.js index c2a1e283..afe25436 100644 --- a/test/deviceReceive.test.js +++ b/test/deviceReceive.test.js @@ -103,6 +103,7 @@ describe('Device receive', () => { jest.advanceTimersByTime(50); expect(MQTT.publish).toHaveBeenCalledTimes(0); jest.runAllTimers(); + await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2, linkquality: 10}); diff --git a/test/networkmap.test.js b/test/networkmap.test.js index 406cd6ea..b1efe36b 100644 --- a/test/networkmap.test.js +++ b/test/networkmap.test.js @@ -15,7 +15,8 @@ const settings = require('../lib/util/settings'); const Controller = require('../lib/controller'); const flushPromises = () => new Promise(setImmediate); const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - +Date.now = jest.fn() +Date.now.mockReturnValue(10000); const mocksClear = [MQTT.publish, logger.warn, logger.debug]; describe('Networkmap', () => { @@ -86,12 +87,12 @@ describe('Networkmap', () => { const expected = { "nodes":[ - {"ieeeAddr":coordinator.ieeeAddr,"friendlyName":"Coordinator","type":"Coordinator","networkAddress":0,"modelID":null,"failed":[]}, - {"ieeeAddr":bulb.ieeeAddr,"friendlyName":"bulb","type":"Router","networkAddress":40369,"modelID":"TRADFRI bulb E27 WS opal 980lm","failed":[]}, - {"ieeeAddr":bulb_color.ieeeAddr,"friendlyName":"bulb_color","type":"Router","networkAddress":40399,"modelID":"LLC020","failed":[]}, - {"ieeeAddr":WXKG02LM.ieeeAddr,"friendlyName":"button_double_key","type":"EndDevice","networkAddress":6538,"modelID":"lumi.sensor_86sw2.es1"}, - {"ieeeAddr":unsupported_router.ieeeAddr,"friendlyName":"0x0017880104e45525","type":"Router","networkAddress":6536,"modelID":"notSupportedModelID","manufacturerName": "Boef","failed":['lqi', 'routingTable']}, - {"ieeeAddr":CC2530_ROUTER.ieeeAddr,"friendlyName":"cc2530_router","type":"Router","networkAddress":6540,"modelID":"lumi.router","failed":[]}, + {"lastSeen": 1000,"ieeeAddr":coordinator.ieeeAddr,"friendlyName":"Coordinator","type":"Coordinator","networkAddress":0,"modelID":null,"failed":[]}, + {"lastSeen": 1000,"ieeeAddr":bulb.ieeeAddr,"friendlyName":"bulb","type":"Router","networkAddress":40369,"modelID":"TRADFRI bulb E27 WS opal 980lm","failed":[]}, + {"lastSeen": 1000,"ieeeAddr":bulb_color.ieeeAddr,"friendlyName":"bulb_color","type":"Router","networkAddress":40399,"modelID":"LLC020","failed":[]}, + {"lastSeen": 1000,"ieeeAddr":WXKG02LM.ieeeAddr,"friendlyName":"button_double_key","type":"EndDevice","networkAddress":6538,"modelID":"lumi.sensor_86sw2.es1"}, + {"lastSeen": 1000,"ieeeAddr":unsupported_router.ieeeAddr,"friendlyName":"0x0017880104e45525","type":"Router","networkAddress":6536,"modelID":"notSupportedModelID","manufacturerName": "Boef","failed":['lqi', 'routingTable']}, + {"lastSeen": 1000,"ieeeAddr":CC2530_ROUTER.ieeeAddr,"friendlyName":"cc2530_router","type":"Router","networkAddress":6540,"modelID":"lumi.router","failed":[]}, ], "links":[ {depth: 1, linkquality: 120, routes: [], source: conv(bulb_color), target: conv(coordinator)}, @@ -128,6 +129,7 @@ describe('Networkmap', () => { it('Output graphviz networkmap', async () => { mock(); const device = zigbeeHerdsman.devices.bulb_color; + device.lastSeen = null; const endpoint = device.getEndpoint(1); const data = {modelID: 'test'} const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; @@ -141,15 +143,15 @@ describe('Networkmap', () => { const expected = `digraph G { node[shape=record]; "0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0)|Last seen: 00:00:00s ago}"]; - "0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (40369)|IKEA TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)|Last seen: unknown}"]; + "0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (40369)|IKEA TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)|Last seen: 00:00:09s ago}"]; "0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 6540)"] - "0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (40399)|Philips Hue Go (7146060PH)|Last seen: 00:00:00s ago}"]; + "0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (40399)|Philips Hue Go (7146060PH)|Last seen: unknown}"]; "0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"] "0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"] - "0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (6538)|Xiaomi Aqara double key wireless wall switch (WXKG02LM)|Last seen: unknown}"]; + "0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (6538)|Xiaomi Aqara double key wireless wall switch (WXKG02LM)|Last seen: 00:00:09s ago}"]; "0x0017880104e45521" -> "0x0017880104e45559" [penwidth=1, weight=0, color="#994444", label="130"] - "0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (6536)failed: lqi,routingTable|Boef notSupportedModelID|Last seen: unknown}"]; - "0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (6540)|Custom devices (DiY) [CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/) (CC2530.ROUTER)|Last seen: unknown}"]; + "0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (6536)failed: lqi,routingTable|Boef notSupportedModelID|Last seen: 00:00:09s ago}"]; + "0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (6540)|Custom devices (DiY) [CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/) (CC2530.ROUTER)|Last seen: 00:00:09s ago}"]; "0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"] }`; diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 9ab2a028..2b97fb86 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -75,6 +75,7 @@ class Device { this.removeFromNetwork = jest.fn(); this.save = jest.fn(); this.manufacturerName = manufacturerName; + this.lastSeen = 1000; } getEndpoint(ID) {