From 856ecf47f15780049285188aa7c5cafcb69b1886 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 4 May 2020 20:06:50 +0200 Subject: [PATCH] Start on new API implementation. #3281 --- lib/controller.js | 2 + lib/extension/bridge.js | 132 ++++++++++++++++++++++++++++++++++++++++ lib/util/utils.js | 11 ++++ lib/zigbee.js | 14 ++++- 4 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 lib/extension/bridge.js diff --git a/lib/controller.js b/lib/controller.js index 3fded4ee3f..d68986f570 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -19,6 +19,7 @@ const ExtensionHomeAssistant = require('./extension/homeassistant'); const ExtensionConfigure = require('./extension/configure'); const ExtensionDeviceGroupMembership = require('./extension/legacy/deviceGroupMembership'); const ExtensionBridgeLegacy = require('./extension/legacy/bridgeLegacy'); +const ExtensionBridge = require('./extension/bridge'); const ExtensionGroups = require('./extension/groups'); const ExtensionAvailability = require('./extension/availability'); const ExtensionBind = require('./extension/bind'); @@ -48,6 +49,7 @@ class Controller { new ExtensionBind(...args), new ExtensionOnEvent(...args), new ExtensionOTAUpdate(...args), + // new ExtensionBridge(...args), ]; /* istanbul ignore else */ diff --git a/lib/extension/bridge.js b/lib/extension/bridge.js new file mode 100644 index 0000000000..e68cb419e8 --- /dev/null +++ b/lib/extension/bridge.js @@ -0,0 +1,132 @@ +/* istanbul ignore file newApi */ + +const logger = require('../util/logger'); +const utils = require('../util/utils'); +const Extension = require('./extension'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); +const settings = require('../util/settings'); + +const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); + +class BridgeLegacy extends Extension { + constructor(zigbee, mqtt, state, publishEntityState, eventBus) { + super(zigbee, mqtt, state, publishEntityState, eventBus); + + + this.requestLookup = { + permitjoin: this.requestPermitJoin.bind(this), + }; + } + + async onMQTTConnected() { + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/+`); + await this.publishInfo(); + await this.publishDevices(); + await this.publishGroups(); + } + + async onMQTTMessage(topic, message) { + const match = topic.match(requestRegex); + if (match && this.requestLookup[match[1].toLowerCase()]) { + message = JSON.parse(message); + const response = await this.requestLookup[match[1].toLowerCase()](message); + await this.mqtt.publish(`bridge/response/${match[1]}`, JSON.stringify(response), {retain: true, qos: 0}); + } + } + + async requestPermitJoin(message) { + const value = typeof message === 'object' ? message.value : message; + await this.zigbee.permitJoin(value); + await this.publishInfo(); + return utils.getResponse(message, {value: value}, null); + } + + async onZigbeeEvent(type, data, resolvedEntity) { + if (['deviceJoined', 'deviceLeave', 'deviceInterview'].includes(type)) { + let payload; + const ieeeAddress = data.device ? data.device.ieeeAddr : data.ieeeAddr; + if (type === 'deviceJoined') payload = {friendlyName: resolvedEntity.settings.friendlyName, ieeeAddress}; + else if (type === 'deviceInterview') { + payload = {friendlyName: resolvedEntity.settings.friendlyName, status: data.status, ieeeAddress}; + if (data.status === 'successful') { + payload.supported = !!resolvedEntity.definition; + payload.definition = resolvedEntity.definition ? { + model: resolvedEntity.definition.model, + vendor: resolvedEntity.definition.vendor, + description: resolvedEntity.definition.description, + supports: resolvedEntity.definition.supports, + } : null; + } + } else payload = {ieeeAddress}; // deviceLeave + + await this.mqtt.publish('bridge/event', JSON.stringify({type, data: payload}), {retain: false, qos: 0}); + } + + if ('deviceLeave' === type || ('deviceInterview' === type && data.status !== 'started')) { + await this.publishDevices(); + } + } + + async publishInfo() { + const info = await utils.getZigbee2mqttVersion(); + const coordinator = await this.zigbee.getCoordinatorVersion(); + const payload = { + version: info.version, + commit: info.commitHash, + coordinator, + logLevel: logger.getLevel(), + permitJoin: await this.zigbee.getPermitJoin(), + }; + + await this.mqtt.publish('bridge/info', JSON.stringify(payload), {retain: true, qos: 0}); + } + + async publishDevices(topic, message) { + const devices = this.zigbee.getClients().map((device) => { + const definition = zigbeeHerdsmanConverters.findByDevice(device); + const resolved = this.zigbee.resolveEntity(device); + const definitionPayload = definition ? { + model: definition.model, + vendor: definition.vendor, + description: definition.description, + supports: definition.supports, + } : null; + + return { + ieeeAddress: device.ieeeAddr, + type: device.type, + networkAddress: device.networkAddress, + supported: !!definition, + friendlyName: resolved.settings.friendlyName, + definition: definitionPayload, + powerSource: device.powerSource, + softwareBuildID: device.softwareBuildID, + dateCode: device.dateCode, + interviewing: device.interviewing, + interviewCompleted: device.interviewCompleted, + }; + }); + + await this.mqtt.publish('bridge/devices', JSON.stringify(devices), {retain: true, qos: 0}); + } + + async publishGroups(topic, message) { + const groups = this.zigbee.getGroups().map((group) => { + const resolved = this.zigbee.resolveEntity(group); + return { + ID: group.groupID, + friendlyName: resolved.settings.friendlyName, + members: group.members.map((m) => { + return { + ieeeAddress: m.deviceIeeeAddress, + endpoint: m.ID, + }; + }), + }; + }); + + await this.mqtt.publish('bridge/groups', JSON.stringify(groups), {retain: true, qos: 0}); + } +} + +module.exports = BridgeLegacy; diff --git a/lib/util/utils.js b/lib/util/utils.js index e2f86df310..de5a1d6658 100644 --- a/lib/util/utils.js +++ b/lib/util/utils.js @@ -120,6 +120,16 @@ function getObjectsProperty(objects, key, defaultValue) { return defaultValue; } +/* istanbul ignore next newApi */ +function getResponse(request, data, error) { + const response = {data, status: error ? 'error' : 'ok'}; + if (error) response.error = error; + if (typeof request === 'object' && request.hasOwnProperty('transaction')) { + response.transaction = request.transaction; + } + return response; +} + module.exports = { millisecondsToSeconds: (milliseconds) => milliseconds / 1000, secondsToMilliseconds: (seconds) => seconds * 1000, @@ -136,4 +146,5 @@ module.exports = { isBatteryPowered: (device) => device.powerSource && device.powerSource === 'Battery', formatDate: (date, type) => formatDate(date, type), equalsPartial, + getResponse, }; diff --git a/lib/zigbee.js b/lib/zigbee.js index ef6ddd4299..1e885ec77b 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -149,9 +149,11 @@ class Zigbee extends events.EventEmitter { * } */ resolveEntity(key) { + /* istanbul ignore next newApi */ assert( typeof key === 'string' || typeof key === 'number' || - key.constructor.name === 'Device', `Wrong type '${typeof key}'`, + key.constructor.name === 'Device' || key.constructor.name === 'Group', + `Wrong type '${typeof key}'`, ); if (typeof key === 'string' || typeof key === 'number') { @@ -215,7 +217,7 @@ class Zigbee extends events.EventEmitter { if (!group) group = this.createGroup(entity.ID); return {type: 'group', group, settings: entity, name: entity.friendlyName}; } - } else { + } /* istanbul ignore else newApi */ else if (key.constructor.name === 'Device') { const setting = settings.getEntity(key.ieeeAddr); return { type: 'device', @@ -225,6 +227,14 @@ class Zigbee extends events.EventEmitter { name: setting ? setting.friendlyName : (key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr), definition: zigbeeHerdsmanConverters.findByDevice(key), }; + } else { // Group + const setting = settings.getEntity(key.groupID); + return { + type: 'group', + group: key, + settings: setting, + name: setting.friendlyName, + }; } }