From 2b8eaa196a59301572575111260e9c30e8b04b20 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 4 Jun 2024 20:05:39 +0200 Subject: [PATCH] fix: Enforce no floating promises (#22880) * Enforce no floating promises. * forEach => for/of. Fix tests. --- .eslintrc.js | 6 +- .gitignore | 14 +--- lib/controller.ts | 4 +- lib/extension/availability.ts | 32 +++---- lib/extension/bind.ts | 6 +- lib/extension/bridge.ts | 72 ++++++++-------- lib/extension/configure.ts | 6 +- lib/extension/externalExtension.ts | 15 ++-- lib/extension/frontend.ts | 2 +- lib/extension/groups.ts | 20 ++--- lib/extension/homeassistant.ts | 84 +++++++++++-------- lib/extension/legacy/bridgeLegacy.ts | 72 ++++++++-------- lib/extension/legacy/deviceGroupMembership.ts | 2 +- lib/extension/networkMap.ts | 2 +- lib/extension/onEvent.ts | 10 +-- lib/extension/otaUpdate.ts | 39 +++++---- lib/extension/publish.ts | 10 +-- lib/extension/receive.ts | 16 ++-- lib/util/utils.ts | 6 +- test/homeassistant.test.js | 21 ++--- test/otaUpdate.test.js | 9 +- test/publish.test.js | 14 ++-- 22 files changed, 231 insertions(+), 231 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 083b59576c..f2abf40f03 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,9 +13,10 @@ module.exports = { 'rules': { 'require-jsdoc': 'off', 'indent': ['error', 4], - 'max-len': ['error', {'code': 120}], + 'max-len': ['error', {'code': 150}], 'no-prototype-builtins': 'off', 'linebreak-style': ['error', (process.platform === 'win32' ? 'windows' : 'unix')], // https://stackoverflow.com/q/39114446/2771889 + '@typescript-eslint/no-floating-promises': 'error', }, 'plugins': [ 'jest', @@ -38,9 +39,10 @@ module.exports = { '@typescript-eslint/semi': ['error'], 'array-bracket-spacing': ['error', 'never'], 'indent': ['error', 4], - 'max-len': ['error', {'code': 120}], + 'max-len': ['error', {'code': 150}], 'no-return-await': 'error', 'object-curly-spacing': ['error', 'never'], + '@typescript-eslint/no-floating-promises': 'error', }, }], }; diff --git a/.gitignore b/.gitignore index 6b1bd6025e..3cfd813f58 100644 --- a/.gitignore +++ b/.gitignore @@ -63,18 +63,6 @@ tsconfig.tsbuildinfo # dotenv environment variables file .env -# data -data/database.db -data/config.json -data/log*.txt -data/state.json -data/log -data-backup/ -data/coordinator_backup.json -data/.storage -data/database.db.backup -data/extension - # MacOS indexing file .DS_Store @@ -82,7 +70,7 @@ data/extension .idea # Ignore config -data/*.yaml +data/* !data/configuration.example.yaml # commit-user-lookup.json diff --git a/lib/controller.ts b/lib/controller.ts index ec32eaf895..722ddb21a6 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -122,7 +122,7 @@ export class Controller { settings.set(['advanced', 'legacy_api'], false); settings.set(['advanced', 'legacy_availability_payload'], false); settings.set(['device_options', 'legacy'], false); - this.enableDisableExtension(false, 'BridgeLegacy'); + await this.enableDisableExtension(false, 'BridgeLegacy'); } // Log zigbee clients on startup @@ -164,7 +164,7 @@ export class Controller { if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) { for (const entity of [...devices, ...this.zigbee.groups()]) { if (this.state.exists(entity)) { - this.publishEntityState(entity, this.state.get(entity), 'publishCached'); + await this.publishEntityState(entity, this.state.get(entity), 'publishCached'); } } } diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index 8cbebe0605..c1dd303317 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -60,9 +60,9 @@ export default class Availability extends Extension { } } - private addToPingQueue(device: Device): void { + private async addToPingQueue(device: Device): Promise { this.pingQueue.push(device); - this.pingQueueExecuteNext(); + await this.pingQueueExecuteNext(); } private removeFromPingQueue(device: Device): void { @@ -99,14 +99,14 @@ export default class Availability extends Extension { return; } - this.publishAvailability(device, !pingedSuccessfully); + await this.publishAvailability(device, !pingedSuccessfully); this.resetTimer(device); this.removeFromPingQueue(device); // Sleep 2 seconds before executing next ping await utils.sleep(2); this.pingQueueExecuting = false; - this.pingQueueExecuteNext(); + await this.pingQueueExecuteNext(); } override async start(): Promise { @@ -114,10 +114,10 @@ export default class Availability extends Extension { throw new Error('This extension cannot be restarted.'); } - this.eventBus.onEntityRenamed(this, (data) => { + this.eventBus.onEntityRenamed(this, async (data) => { if (utils.isAvailabilityEnabledForEntity(data.entity, settings.get())) { - this.mqtt.publish(`${data.from}/availability`, null, {retain: true, qos: 1}); - this.publishAvailability(data.entity, false, true); + await this.mqtt.publish(`${data.from}/availability`, null, {retain: true, qos: 1}); + await this.publishAvailability(data.entity, false, true); } }); @@ -127,29 +127,29 @@ export default class Availability extends Extension { this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged); this.eventBus.onPublishAvailability(this, this.publishAvailabilityForAllEntities); this.eventBus.onGroupMembersChanged(this, (data) => this.publishAvailability(data.group, false)); - this.publishAvailabilityForAllEntities(); + await this.publishAvailabilityForAllEntities(); } - @bind private publishAvailabilityForAllEntities(): void { + @bind private async publishAvailabilityForAllEntities(): Promise { for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) { if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) { // Publish initial availability - this.publishAvailability(entity, true, false, true); + await this.publishAvailability(entity, true, false, true); if (entity.isDevice()) { this.resetTimer(entity); // If an active device is initially unavailable, ping it. if (this.isActiveDevice(entity) && !this.isAvailable(entity)) { - this.addToPingQueue(entity); + await this.addToPingQueue(entity); } } } } } - private publishAvailability(entity: Device | Group, logLastSeen: boolean, - forcePublish=false, skipGroups=false): void { + private async publishAvailability(entity: Device | Group, logLastSeen: boolean, + forcePublish=false, skipGroups=false): Promise { if (logLastSeen && entity.isDevice()) { const ago = Date.now() - entity.zh.lastSeen; if (this.isActiveDevice(entity)) { @@ -175,7 +175,7 @@ export default class Availability extends Extension { const topic = `${entity.name}/availability`; const payload = utils.availabilityPayload(available ? 'online' : 'offline', settings.get()); this.availabilityCache[entity.ID] = available; - this.mqtt.publish(topic, payload, {retain: true, qos: 1}); + await this.mqtt.publish(topic, payload, {retain: true, qos: 1}); if (!skipGroups && entity.isDevice()) { this.zigbee.groups().filter((g) => g.hasMember(entity)) @@ -184,12 +184,12 @@ export default class Availability extends Extension { } } - @bind private onLastSeenChanged(data: eventdata.LastSeenChanged): void { + @bind private async onLastSeenChanged(data: eventdata.LastSeenChanged): Promise { if (utils.isAvailabilityEnabledForEntity(data.device, settings.get())) { // Remove from ping queue, not necessary anymore since we know the device is online. this.removeFromPingQueue(data.device); this.resetTimer(data.device); - this.publishAvailability(data.device, false); + await this.publishAvailability(data.device, false); } } diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 36ba776c97..4905862fd1 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -285,7 +285,7 @@ export default class Bind extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_${type}`, message: {from: source.name, to: target.name, cluster}}), @@ -300,7 +300,7 @@ export default class Bind extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_${type}_failed`, message: {from: source.name, to: target.name, cluster}}), @@ -316,7 +316,7 @@ export default class Bind extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_${type}_failed`, message: {from: source.name, to: target.name}}), ); diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 01137ea53b..ca766bc321 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -72,7 +72,7 @@ export default class Bridge extends Extension { if (payload !== this.lastBridgeLoggingPayload) { this.lastBridgeLoggingPayload = payload; - this.mqtt.publish(`bridge/logging`, payload, {}, baseTopic, true); + void this.mqtt.publish(`bridge/logging`, payload, {}, baseTopic, true); } }; @@ -111,38 +111,38 @@ export default class Bridge extends Extension { this.publishInfo() && this.publishDefinitions()); this.eventBus.onPermitJoinChanged(this, () => !this.zigbee.isStopping() && this.publishInfo()); - this.eventBus.onScenesChanged(this, () => { - this.publishDevices(); - this.publishGroups(); + this.eventBus.onScenesChanged(this, async () => { + await this.publishDevices(); + await this.publishGroups(); }); // Zigbee events - const publishEvent = (type: string, data: KeyValue): Promise => + const publishEvent = async (type: string, data: KeyValue): Promise => this.mqtt.publish('bridge/event', stringify({type, data}), {retain: false, qos: 0}); - this.eventBus.onDeviceJoined(this, (data) => { + this.eventBus.onDeviceJoined(this, async (data) => { this.lastJoinedDeviceIeeeAddr = data.device.ieeeAddr; - this.publishDevices(); - publishEvent('device_joined', {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}); + await this.publishDevices(); + await publishEvent('device_joined', {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}); }); - this.eventBus.onDeviceLeave(this, (data) => { - this.publishDevices(); - this.publishDefinitions(); - publishEvent('device_leave', {ieee_address: data.ieeeAddr, friendly_name: data.name}); + this.eventBus.onDeviceLeave(this, async (data) => { + await this.publishDevices(); + await this.publishDefinitions(); + await publishEvent('device_leave', {ieee_address: data.ieeeAddr, friendly_name: data.name}); }); this.eventBus.onDeviceNetworkAddressChanged(this, () => this.publishDevices()); - this.eventBus.onDeviceInterview(this, (data) => { - this.publishDevices(); + this.eventBus.onDeviceInterview(this, async (data) => { + await this.publishDevices(); const payload: KeyValue = {friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr}; if (data.status === 'successful') { payload.supported = data.device.isSupported; payload.definition = this.getDefinitionPayload(data.device); } - publishEvent('device_interview', payload); + await publishEvent('device_interview', payload); }); - this.eventBus.onDeviceAnnounce(this, (data) => { - this.publishDevices(); - publishEvent('device_announce', {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}); + this.eventBus.onDeviceAnnounce(this, async (data) => { + await this.publishDevices(); + await publishEvent('device_announce', {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}); }); await this.publishInfo(); @@ -154,7 +154,7 @@ export default class Bridge extends Extension { } override async stop(): Promise { - super.stop(); + await super.stop(); logger.removeTransport(this.logTransport); } @@ -219,7 +219,7 @@ export default class Bridge extends Extension { } logger.info('Successfully changed options'); - this.publishInfo(); + await this.publishInfo(); return utils.getResponse(message, {restart_required: this.restartRequired}, null); } @@ -252,7 +252,7 @@ export default class Bridge extends Extension { const ID = typeof message === 'object' && message.hasOwnProperty('id') ? message.id : null; const group = settings.addGroup(friendlyName, ID); this.zigbee.createGroup(group.ID); - this.publishGroups(); + await this.publishGroups(); return utils.getResponse(message, {friendly_name: group.friendly_name, id: group.ID}, null); } @@ -336,7 +336,7 @@ export default class Bridge extends Extension { } settings.set(['advanced', 'last_seen'], value); - this.publishInfo(); + await this.publishInfo(); return utils.getResponse(message, {value}, null); } @@ -350,7 +350,7 @@ export default class Bridge extends Extension { await this.enableDisableExtension(value, 'HomeAssistant'); settings.set(['homeassistant'], value); - this.publishInfo(); + await this.publishInfo(); return utils.getResponse(message, {value}, null); } @@ -363,7 +363,7 @@ export default class Bridge extends Extension { } settings.set(['advanced', 'elapsed'], value); - this.publishInfo(); + await this.publishInfo(); return utils.getResponse(message, {value}, null); } @@ -375,7 +375,7 @@ export default class Bridge extends Extension { } logger.setLevel(value); - this.publishInfo(); + await this.publishInfo(); return utils.getResponse(message, {value}, null); } @@ -490,7 +490,7 @@ export default class Bridge extends Extension { maximumReportInterval: message.maximum_report_interval, reportableChange: message.reportable_change, }], message.options); - this.publishDevices(); + await this.publishDevices(); logger.info(`Configured reporting for '${message.id}', '${message.cluster}.${message.attribute}'`); @@ -556,19 +556,19 @@ export default class Bridge extends Extension { settings.changeFriendlyName(from, to); // Clear retained messages - this.mqtt.publish(oldFriendlyName, '', {retain: true}); + await this.mqtt.publish(oldFriendlyName, '', {retain: true}); this.eventBus.emitEntityRenamed({entity: entity, homeAssisantRename, from: oldFriendlyName, to}); if (entity instanceof Device) { - this.publishDevices(); + await this.publishDevices(); } else { - this.publishGroups(); - this.publishInfo(); + await this.publishGroups(); + await this.publishInfo(); } // Republish entity state - this.publishEntityState(entity, {}); + await this.publishEntityState(entity, {}); return utils.getResponse( message, @@ -635,18 +635,18 @@ export default class Bridge extends Extension { this.state.remove(entityID); // Clear any retained messages - this.mqtt.publish(friendlyName, '', {retain: true}); + await this.mqtt.publish(friendlyName, '', {retain: true}); logger.info(`Successfully removed ${entityType} '${friendlyName}'${blockForceLog}`); if (entity instanceof Device) { - this.publishGroups(); - this.publishDevices(); + await this.publishGroups(); + await this.publishDevices(); // Refresh Cluster definition - this.publishDefinitions(); + await this.publishDefinitions(); return utils.getResponse(message, {id: ID, block, force}, null); } else { - this.publishGroups(); + await this.publishGroups(); return utils.getResponse(message, {id: ID, force: force}, null); } } catch (error) { diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index 6af5a694c1..84c22cd4b7 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -39,7 +39,7 @@ export default class Configure extends Extension { return; } - this.configure(device, 'mqtt_message', true); + await this.configure(device, 'mqtt_message', true); } else if (data.topic === this.topic) { const message = utils.parseJSON(data.message, data.message); const ID = typeof message === 'object' && message.hasOwnProperty('id') ? message.id : message; @@ -75,13 +75,13 @@ export default class Configure extends Extension { } }); - this.eventBus.onDeviceJoined(this, (data) => { + this.eventBus.onDeviceJoined(this, async (data) => { if (data.device.zh.meta.hasOwnProperty('configured')) { delete data.device.zh.meta.configured; data.device.zh.save(); } - this.configure(data.device, 'zigbee_event'); + await this.configure(data.device, 'zigbee_event'); }); this.eventBus.onDeviceInterview(this, (data) => this.configure(data.device, 'zigbee_event')); this.eventBus.onLastSeenChanged(this, (data) => this.configure(data.device, 'zigbee_event')); diff --git a/lib/extension/externalExtension.ts b/lib/extension/externalExtension.ts index 4115ec8eeb..a65c664cca 100644 --- a/lib/extension/externalExtension.ts +++ b/lib/extension/externalExtension.ts @@ -16,7 +16,7 @@ export default class ExternalExtension extends Extension { override async start(): Promise { this.eventBus.onMQTTMessage(this, this.onMQTTMessage); this.requestLookup = {'save': this.saveExtension, 'remove': this.removeExtension}; - this.loadUserDefinedExtensions(); + await this.loadUserDefinedExtensions(); await this.publishExtensions(); } @@ -46,7 +46,7 @@ export default class ExternalExtension extends Extension { const basePath = this.getExtensionsBasePath(); const extensionFilePath = path.join(basePath, path.basename(name)); fs.unlinkSync(extensionFilePath); - this.publishExtensions(); + await this.publishExtensions(); logger.info(`Extension ${name} removed`); return utils.getResponse(message, {}, null); } else { @@ -65,7 +65,7 @@ export default class ExternalExtension extends Extension { } const extensionFilePath = path.join(basePath, path.basename(name)); fs.writeFileSync(extensionFilePath, code); - this.publishExtensions(); + await this.publishExtensions(); logger.info(`Extension ${name} loaded`); return utils.getResponse(message, {}, null); } @@ -92,11 +92,10 @@ export default class ExternalExtension extends Extension { this.eventBus, settings, logger)); } - private loadUserDefinedExtensions(): void { - const extensions = this.getListOfUserDefinedExtensions(); - extensions - .map(({code, name}) => utils.loadModuleFromText(code, name)) - .map(this.loadExtension); + private async loadUserDefinedExtensions(): Promise { + for (const extension of this.getListOfUserDefinedExtensions()) { + await this.loadExtension(utils.loadModuleFromText(extension.code, extension.name) as typeof Extension); + } } private async publishExtensions(): Promise { diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index 0957ce74d8..babc4c59c3 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -87,7 +87,7 @@ export default class Frontend extends Extension { } override async stop(): Promise { - super.stop(); + await super.stop(); this.wss?.clients.forEach((client) => { client.send(stringify({topic: 'bridge/state', payload: 'offline'})); client.terminate(); diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index b090e5ba66..14139592e2 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -80,7 +80,7 @@ export default class Groups extends Extension { // In settings but not in zigbee for (const entity of settingsEndpoint) { if (!zigbeeGroup.zh.hasMember(entity.endpoint)) { - addRemoveFromGroup('add', entity.name, settingGroup.friendly_name, entity.endpoint, zigbeeGroup); + await addRemoveFromGroup('add', entity.name, settingGroup.friendly_name, entity.endpoint, zigbeeGroup); } } @@ -88,7 +88,7 @@ export default class Groups extends Extension { for (const endpoint of zigbeeGroup.zh.members) { if (!settingsEndpoint.find((e) => e.endpoint === endpoint)) { const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name; - addRemoveFromGroup('remove', deviceName, settingGroup.friendly_name, endpoint, zigbeeGroup); + await addRemoveFromGroup('remove', deviceName, settingGroup.friendly_name, endpoint, zigbeeGroup); } } } @@ -97,7 +97,7 @@ export default class Groups extends Extension { if (!settingsGroups.find((g) => g.ID === zigbeeGroup.ID)) { for (const endpoint of zigbeeGroup.zh.members) { const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name; - addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup); + await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup); } } } @@ -200,7 +200,7 @@ export default class Groups extends Extension { return true; } - private parseMQTTMessage(data: eventdata.MQTTMessage): ParsedMQTTMessage { + private async parseMQTTMessage(data: eventdata.MQTTMessage): Promise { let type: 'remove' | 'add' | 'remove_all' = null; let resolvedEntityGroup: Group = null; let resolvedEntityDevice: Device = null; @@ -229,7 +229,7 @@ export default class Groups extends Extension { if (settings.get().advanced.legacy_api) { const payload = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: 'group doesn\'t exists'}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_group_${type}_failed`, message: payload}), ); @@ -251,7 +251,7 @@ export default class Groups extends Extension { const payload = { friendly_name: data.message, group: legacyTopicRegexMatch[1], error: 'entity doesn\'t exists', }; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_group_${type}_failed`, message: payload}), ); @@ -299,7 +299,7 @@ export default class Groups extends Extension { } @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const parsed = this.parseMQTTMessage(data); + const parsed = await this.parseMQTTMessage(data); if (!parsed || !parsed.type) return; let { resolvedEntityGroup, resolvedEntityDevice, type, error, triggeredViaLegacyApi, @@ -340,7 +340,7 @@ export default class Groups extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_group_add`, message: payload}), ); @@ -354,7 +354,7 @@ export default class Groups extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_group_remove`, message: payload}), ); @@ -369,7 +369,7 @@ export default class Groups extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const payload = {friendly_name: resolvedEntityDevice.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_group_remove_all`, message: payload}), ); diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 3c7e1bd915..da684e7c9b 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -152,8 +152,8 @@ export default class HomeAssistant extends Extension { this.eventBus.onDeviceInterview(this, this.onZigbeeEvent); this.eventBus.onDeviceMessage(this, this.onZigbeeEvent); this.eventBus.onScenesChanged(this, this.onScenesChanged); - this.eventBus.onEntityOptionsChanged(this, (data) => this.discover(data.entity)); - this.eventBus.onExposesChanged(this, (data) => this.discover(data.device)); + this.eventBus.onEntityOptionsChanged(this, async (data) => this.discover(data.entity)); + this.eventBus.onExposesChanged(this, async (data) => this.discover(data.device)); this.mqtt.subscribe(this.statusTopic); this.mqtt.subscribe(defaultStatusTopic); @@ -166,13 +166,19 @@ export default class HomeAssistant extends Extension { const discoverWait = 5; // Discover with `published = false`, this will populate `this.discovered` without publishing the discoveries. // This is needed for clearing outdated entries in `this.onMQTTMessage()` - [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()].forEach((e) => this.discover(e, false)); + for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) { + await this.discover(e, false); + } + logger.debug(`Discovering entities to Home Assistant in ${discoverWait}s`); this.mqtt.subscribe(`${this.discoveryTopic}/#`); - setTimeout(() => { + setTimeout(async () => { this.mqtt.unsubscribe(`${this.discoveryTopic}/#`); logger.debug(`Discovering entities to Home Assistant`); - [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()].forEach((e) => this.discover(e)); + + for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) { + await this.discover(e); + } }, utils.seconds(discoverWait)); // Send availability messages, this is required if the legacy_availability_payload option has been changed. @@ -1137,18 +1143,19 @@ export default class HomeAssistant extends Extension { return discoveryEntries; } - @bind onDeviceRemoved(data: eventdata.DeviceRemoved): void { + @bind async onDeviceRemoved(data: eventdata.DeviceRemoved): Promise { logger.debug(`Clearing Home Assistant discovery for '${data.name}'`); const discovered = this.getDiscovered(data.ieeeAddr); - Object.keys(discovered.messages).forEach((topic) => { - this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); - }); + + for (const topic of Object.keys(discovered.messages)) { + await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); + } delete this.discovered[data.ieeeAddr]; } - @bind onGroupMembersChanged(data: eventdata.GroupMembersChanged): void { - this.discover(data.group); + @bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise { + await this.discover(data.group); } @bind async onPublishEntityState(data: eventdata.PublishEntityState): Promise { @@ -1162,7 +1169,7 @@ export default class HomeAssistant extends Extension { */ const entity = this.zigbee.resolveEntity(data.entity.name); if (entity.isDevice()) { - Object.keys(this.getDiscovered(entity).messages).forEach((topic) => { + for (const topic of Object.keys(this.getDiscovered(entity).messages)) { const objectID = topic.match(this.discoveryRegexWoTopic)?.[3]; const lightMatch = /^light_(.*)/.exec(objectID); const coverMatch = /^cover_(.*)/.exec(objectID); @@ -1180,9 +1187,9 @@ export default class HomeAssistant extends Extension { } } - this.mqtt.publish(`${data.entity.name}/${endpoint}`, stringify(payload), {}); + await this.mqtt.publish(`${data.entity.name}/${endpoint}`, stringify(payload), {}); } - }); + } } /** @@ -1193,7 +1200,7 @@ export default class HomeAssistant extends Extension { if (settings.get().homeassistant.legacy_triggers) { const keys = ['action', 'click'].filter((k) => data.message[k]); for (const key of keys) { - this.publishEntityState(data.entity, {[key]: ''}); + await this.publishEntityState(data.entity, {[key]: ''}); } } @@ -1221,7 +1228,7 @@ export default class HomeAssistant extends Extension { if (data.homeAssisantRename) { const discovered = this.getDiscovered(data.entity); for (const topic of Object.keys(discovered.messages)) { - this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); + await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); } discovered.messages = {}; @@ -1230,13 +1237,13 @@ export default class HomeAssistant extends Extension { await utils.sleep(2); } - this.discover(data.entity); + await this.discover(data.entity); if (data.entity.isDevice()) { for (const config of this.getDiscovered(data.entity).triggers) { const key = config.substring(0, config.indexOf('_')); const value = config.substring(config.indexOf('_') + 1); - this.publishDeviceTriggerDiscover(data.entity, key, value, true); + await this.publishDeviceTriggerDiscover(data.entity, key, value, true); } } } @@ -1417,7 +1424,7 @@ export default class HomeAssistant extends Extension { return configs; } - private discover(entity: Device | Group | Bridge, publish: boolean = true): void { + private async discover(entity: Device | Group | Bridge, publish: boolean = true): Promise { // Handle type differences. const isDevice = entity.isDevice(); const isGroup = entity.isGroup(); @@ -1431,9 +1438,10 @@ export default class HomeAssistant extends Extension { const discovered = this.getDiscovered(entity); discovered.discovered = true; - const lastDiscoverdTopics = Object.keys(discovered.messages); + const lastDiscoveredTopics = Object.keys(discovered.messages); const newDiscoveredTopics: Set = new Set(); - this.getConfigs(entity).forEach((config) => { + + for (const config of this.getConfigs(entity)) { const payload = {...config.discovery_payload}; const baseTopic = `${settings.get().mqtt.base_topic}/${entity.name}`; let stateTopic = baseTopic; @@ -1607,7 +1615,7 @@ export default class HomeAssistant extends Extension { } // Override configuration with user settings. - if (entity.options.hasOwnProperty('homeassistant')) { + if (entity.options.homeassistant != undefined) { const add = (obj: KeyValue, ignoreName: boolean): void => { Object.keys(obj).forEach((key) => { if (['type', 'object_id'].includes(key)) { @@ -1629,7 +1637,7 @@ export default class HomeAssistant extends Extension { add(entity.options.homeassistant, true); - if (entity.options.homeassistant.hasOwnProperty(config.object_id)) { + if (entity.options.homeassistant[config.object_id] != undefined) { add(entity.options.homeassistant[config.object_id], false); } } @@ -1643,22 +1651,23 @@ export default class HomeAssistant extends Extension { if (!discoveredMessage || discoveredMessage.payload !== payloadStr || !discoveredMessage.published) { discovered.messages[topic] = {payload: payloadStr, published: publish}; if (publish) { - this.mqtt.publish(topic, payloadStr, {retain: true, qos: 1}, this.discoveryTopic, false, false); + await this.mqtt.publish(topic, payloadStr, {retain: true, qos: 1}, this.discoveryTopic, false, false); } } else { logger.debug(`Skipping discovery of '${topic}', already discovered`); } config.mockProperties?.forEach((mockProperty) => discovered.mockProperties.add(mockProperty)); - }); - lastDiscoverdTopics.forEach((topic) => { + } + + for (const topic of lastDiscoveredTopics) { const isDeviceAutomation = topic.match(this.discoveryRegexWoTopic)[1] === 'device_automation'; if (!newDiscoveredTopics.has(topic) && !isDeviceAutomation) { - this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); + await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); } - }); + } } - @bind private onMQTTMessage(data: eventdata.MQTTMessage): void { + @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { const discoveryMatch = data.topic.match(this.discoveryRegex); const isDeviceAutomation = discoveryMatch && discoveryMatch[1] === 'device_automation'; if (discoveryMatch) { @@ -1703,7 +1712,7 @@ export default class HomeAssistant extends Extension { if (clear) { logger.debug(`Clearing outdated Home Assistant config '${data.topic}'`); - this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); + await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); } else { this.getDiscovered(entity).messages[topic] = {payload: stringify(message), published: true}; } @@ -1713,7 +1722,7 @@ export default class HomeAssistant extends Extension { // Publish all device states. for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) { if (this.state.exists(entity)) { - this.publishEntityState(entity, this.state.get(entity), 'publishCached'); + await this.publishEntityState(entity, this.state.get(entity), 'publishCached'); } } @@ -1722,9 +1731,9 @@ export default class HomeAssistant extends Extension { } } - @bind onZigbeeEvent(data: {device: Device}): void { + @bind async onZigbeeEvent(data: {device: Device}): Promise { if (!this.getDiscovered(data.device).discovered) { - this.discover(data.device); + await this.discover(data.device); } } @@ -1734,12 +1743,13 @@ export default class HomeAssistant extends Extension { // First, clear existing scene discovery topics logger.debug(`Clearing Home Assistant scene discovery for '${data.entity.name}'`); const discovered = this.getDiscovered(data.entity); - Object.keys(discovered.messages).forEach((topic) => { + + for (const topic of Object.keys(discovered.messages)) { if (topic.startsWith('scene')) { - this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); + await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); delete discovered.messages[topic]; } - }); + } // Make sure Home Assistant deletes the old entity first otherwise another one (_2) is created // https://github.com/Koenkk/zigbee2mqtt/issues/12610 @@ -1748,7 +1758,7 @@ export default class HomeAssistant extends Extension { // Re-discover entity (including any new scenes). logger.debug(`Re-discovering entities with their scenes.`); - this.discover(data.entity); + await this.discover(data.entity); } private getDevicePayload(entity: Device | Group | Bridge): KeyValue { diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts index 9a40eb2837..d46b0f16e2 100644 --- a/lib/extension/legacy/bridgeLegacy.ts +++ b/lib/extension/legacy/bridgeLegacy.ts @@ -45,13 +45,13 @@ export default class BridgeLegacy extends Extension { await this.publish(); } - @bind whitelist(topic: string, message: string): void { + @bind async whitelist(topic: string, message: string): Promise { try { const entity = settings.getDevice(message); assert(entity, `Entity '${message}' does not exist`); settings.addDeviceToPasslist(entity.ID.toString()); logger.info(`Whitelisted '${entity.friendly_name}'`); - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: 'device_whitelisted', message: {friendly_name: entity.friendly_name}}), ); @@ -82,7 +82,7 @@ export default class BridgeLegacy extends Extension { @bind async permitJoin(topic: string, message: string): Promise { await this.zigbee.permitJoin(message.toLowerCase() === 'true'); - this.publish(); + await this.publish(); } @bind async reset(): Promise { @@ -116,7 +116,7 @@ export default class BridgeLegacy extends Extension { logger.info(`Set elapsed to ${message}`); } - @bind logLevel(topic: string, message: string): void { + @bind async logLevel(topic: string, message: string): Promise { const level = message.toLowerCase() as settings.LogLevel; if (settings.LOG_LEVELS.includes(level)) { logger.info(`Switching log level to '${level}'`); @@ -125,7 +125,7 @@ export default class BridgeLegacy extends Extension { logger.error(`Could not set log level to '${level}'. Allowed level: '${settings.LOG_LEVELS.join(',')}'`); } - this.publish(); + await this.publish(); } @bind async devices(topic: string): Promise { @@ -162,23 +162,23 @@ export default class BridgeLegacy extends Extension { }); if (topic.split('/').pop() == 'get') { - this.mqtt.publish( + await this.mqtt.publish( `bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false, ); } else { - this.mqtt.publish('bridge/log', stringify({type: 'devices', message: devices})); + await this.mqtt.publish('bridge/log', stringify({type: 'devices', message: devices})); } } - @bind groups(): void { + @bind async groups(): Promise { const payload = settings.getGroups().map((g) => { return {...g, ID: Number(g.ID)}; }); - this.mqtt.publish('bridge/log', stringify({type: 'groups', message: payload})); + await this.mqtt.publish('bridge/log', stringify({type: 'groups', message: payload})); } - @bind rename(topic: string, message: string): void { + @bind async rename(topic: string, message: string): Promise { const invalid = `Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`; @@ -196,19 +196,19 @@ export default class BridgeLegacy extends Extension { return; } - this._renameInternal(json.old, json.new); + await this._renameInternal(json.old, json.new); } - @bind renameLast(topic: string, message: string): void { + @bind async renameLast(topic: string, message: string): Promise { if (!this.lastJoinedDeviceName) { logger.error(`Cannot rename last joined device, no device has joined during this session`); return; } - this._renameInternal(this.lastJoinedDeviceName, message); + await this._renameInternal(this.lastJoinedDeviceName, message); } - _renameInternal(from: string, to: string): void { + async _renameInternal(from: string, to: string): Promise { try { const isGroup = settings.getGroup(from) !== null; settings.changeFriendlyName(from, to); @@ -218,7 +218,7 @@ export default class BridgeLegacy extends Extension { this.eventBus.emitEntityRenamed({homeAssisantRename: false, from, to, entity}); } - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}), ); @@ -227,7 +227,7 @@ export default class BridgeLegacy extends Extension { } } - @bind addGroup(topic: string, message: string): void { + @bind async addGroup(topic: string, message: string): Promise { let id = null; let name = null; try { @@ -252,11 +252,11 @@ export default class BridgeLegacy extends Extension { const group = settings.addGroup(name, id); this.zigbee.createGroup(group.ID); - this.mqtt.publish('bridge/log', stringify({type: `group_added`, message: name})); + await this.mqtt.publish('bridge/log', stringify({type: `group_added`, message: name})); logger.info(`Added group '${name}'`); } - @bind removeGroup(topic: string, message: string): void { + @bind async removeGroup(topic: string, message: string): Promise { const name = message; const entity = this.zigbee.resolveEntity(message) as Group; assert(entity && entity.isGroup(), `Group '${message}' does not exist`); @@ -264,11 +264,11 @@ export default class BridgeLegacy extends Extension { if (topic.includes('force')) { entity.zh.removeFromDatabase(); } else { - entity.zh.removeFromNetwork(); + await entity.zh.removeFromNetwork(); } settings.removeGroup(message); - this.mqtt.publish('bridge/log', stringify({type: `group_removed`, message})); + await this.mqtt.publish('bridge/log', stringify({type: `group_removed`, message})); logger.info(`Removed group '${name}'`); } @@ -295,14 +295,14 @@ export default class BridgeLegacy extends Extension { if (!entity) { logger.error(`Cannot ${lookup[action][2]}, device '${message}' does not exist`); - this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message})); + await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message})); return; } const ieeeAddr = entity.ieeeAddr; const name = entity.name; - const cleanup = (): void => { + const cleanup = async (): Promise => { // Fire event this.eventBus.emitDeviceRemoved({ieeeAddr, name}); @@ -313,7 +313,7 @@ export default class BridgeLegacy extends Extension { this.state.remove(ieeeAddr); logger.info(`Successfully ${lookup[action][0]} ${entity.name}`); - this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}`, message})); + await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}`, message})); }; try { @@ -324,13 +324,13 @@ export default class BridgeLegacy extends Extension { await entity.zh.removeFromNetwork(); } - cleanup(); + await cleanup(); } catch (error) { logger.error(`Failed to ${lookup[action][2]} ${entity.name} (${error})`); // eslint-disable-next-line logger.error(`See https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html#zigbee2mqtt-bridge-request for more info`); - this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message})); + await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message})); } if (action === 'ban') { @@ -371,13 +371,13 @@ export default class BridgeLegacy extends Extension { await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}); } - onZigbeeEvent_(type: string, data: KeyValue, resolvedEntity: Device): void { + async onZigbeeEvent_(type: string, data: KeyValue, resolvedEntity: Device): Promise { if (type === 'deviceJoined' && resolvedEntity) { this.lastJoinedDeviceName = resolvedEntity.name; } if (type === 'deviceJoined') { - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_connected`, message: {friendly_name: resolvedEntity.name}}), ); @@ -386,20 +386,20 @@ export default class BridgeLegacy extends Extension { if (resolvedEntity.isSupported) { const {vendor, description, model} = resolvedEntity.definition; const log = {friendly_name: resolvedEntity.name, model, vendor, description, supported: true}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta: log}), ); } else { const meta = {friendly_name: resolvedEntity.name, supported: false}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta}), ); } } else if (data.status === 'failed') { const meta = {friendly_name: resolvedEntity.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `pairing`, message: 'interview_failed', meta}), ); @@ -407,7 +407,7 @@ export default class BridgeLegacy extends Extension { /* istanbul ignore else */ if (data.status === 'started') { const meta = {friendly_name: resolvedEntity.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `pairing`, message: 'interview_started', meta}), ); @@ -415,13 +415,13 @@ export default class BridgeLegacy extends Extension { } } else if (type === 'deviceAnnounce') { const meta = {friendly_name: resolvedEntity.name}; - this.mqtt.publish('bridge/log', stringify({type: `device_announced`, message: 'announce', meta})); + await this.mqtt.publish('bridge/log', stringify({type: `device_announced`, message: 'announce', meta})); } else { /* istanbul ignore else */ if (type === 'deviceLeave') { const name = data.ieeeAddr; const meta = {friendly_name: name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `device_removed`, message: 'left_network', meta}), ); @@ -431,7 +431,7 @@ export default class BridgeLegacy extends Extension { @bind async touchlinkFactoryReset(): Promise { logger.info('Starting touchlink factory reset...'); - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `touchlink`, message: 'reset_started', meta: {status: 'started'}}), ); @@ -439,13 +439,13 @@ export default class BridgeLegacy extends Extension { if (result) { logger.info('Successfully factory reset device through Touchlink'); - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `touchlink`, message: 'reset_success', meta: {status: 'success'}}), ); } else { logger.warning('Failed to factory reset device through Touchlink'); - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `touchlink`, message: 'reset_failed', meta: {status: 'failed'}}), ); diff --git a/lib/extension/legacy/deviceGroupMembership.ts b/lib/extension/legacy/deviceGroupMembership.ts index 21750a57ac..2697940d8c 100644 --- a/lib/extension/legacy/deviceGroupMembership.ts +++ b/lib/extension/legacy/deviceGroupMembership.ts @@ -56,6 +56,6 @@ export default class DeviceGroupMembership extends Extension { } logger.info(`${msgGroupList} and ${msgCapacity}`); - this.publishEntityState(device, {group_list: grouplist, group_capacity: capacity}); + await this.publishEntityState(device, {group_list: grouplist, group_capacity: capacity}); } } diff --git a/lib/extension/networkMap.ts b/lib/extension/networkMap.ts index 7a97d118e4..2370c05112 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -47,7 +47,7 @@ export default class NetworkMap extends Extension { const topology = await this.networkScan(includeRoutes); let converted = this.supportedFormats[data.message](topology); converted = data.message === 'raw' ? stringify(converted) : converted; - this.mqtt.publish(`bridge/networkmap/${data.message}`, converted as string, {}); + await this.mqtt.publish(`bridge/networkmap/${data.message}`, converted as string, {}); } } diff --git a/lib/extension/onEvent.ts b/lib/extension/onEvent.ts index 74979c50cd..db84068868 100644 --- a/lib/extension/onEvent.ts +++ b/lib/extension/onEvent.ts @@ -7,7 +7,7 @@ import Extension from './extension'; export default class OnEvent extends Extension { override async start(): Promise { for (const device of this.zigbee.devices(false)) { - this.callOnEvent(device, 'start', {}); + await this.callOnEvent(device, 'start', {}); } this.eventBus.onDeviceMessage(this, (data) => this.callOnEvent(data.device, 'message', this.convertData(data))); @@ -20,9 +20,9 @@ export default class OnEvent extends Extension { this.eventBus.onDeviceNetworkAddressChanged(this, (data) => this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data))); this.eventBus.onEntityOptionsChanged(this, - (data) => { + async (data) => { if (data.entity.isDevice()) { - this.callOnEvent(data.entity, 'deviceOptionsChanged', data) + await this.callOnEvent(data.entity, 'deviceOptionsChanged', data) .then(() => this.eventBus.emitDevicesChanged()); } }); @@ -33,7 +33,7 @@ export default class OnEvent extends Extension { } override async stop(): Promise { - super.stop(); + await super.stop(); for (const device of this.zigbee.devices(false)) { await this.callOnEvent(device, 'stop', {}); } @@ -41,7 +41,7 @@ export default class OnEvent extends Extension { private async callOnEvent(device: Device, type: zhc.OnEventType, data: KeyValue): Promise { const state = this.state.get(device); - zhc.onEvent(type, data, device.zh); + await zhc.onEvent(type, data, device.zh); if (device.definition?.onEvent) { const options: KeyValue = device.options; diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index d74c36480f..fd28e1e24c 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -9,6 +9,7 @@ import dataDir from '../util/data'; import * as URI from 'uri-js'; import path from 'path'; import * as zhc from 'zigbee-herdsman-converters'; +import {Zcl} from 'zigbee-herdsman'; function isValidUrl(url: string): boolean { let parsed; @@ -96,16 +97,14 @@ export default class OTAUpdate extends Extension { this.lastChecked[data.device.ieeeAddr] = Date.now(); let availableResult: zhc.OtaUpdateAvailableResult = null; try { - // @ts-expect-error typing guaranteed by data.type - const dataData: zhc.ota.ImageInfo = data.data; - availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, dataData); + availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, data.data as zhc.ota.ImageInfo); } catch (e) { supportsOTA = false; logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`); } const payload = this.getEntityPublishPayload(data.device, availableResult ?? 'idle'); - this.publishEntityState(data.device, payload); + await this.publishEntityState(data.device, payload); if (availableResult?.available) { const message = `Update available for '${data.device.name}'`; @@ -114,7 +113,7 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: 'available', device: data.device.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `ota_update`, message, meta}), ); @@ -122,10 +121,10 @@ export default class OTAUpdate extends Extension { } } - // Respond to the OTA request: respond with NO_IMAGE_AVAILABLE (0x98) (so the client stops requesting OTAs) + // Respond to stop the client from requesting OTAs const endpoint = data.device.zh.endpoints.find((e) => e.supportsOutputCluster('genOta')) || data.endpoint; await endpoint.commandResponse('genOta', 'queryNextImageResponse', - {status: 0x98}, undefined, data.meta.zclTransactionSequenceNumber); + {status: Zcl.Status.NO_IMAGE_AVAILABLE}, undefined, data.meta.zclTransactionSequenceNumber); logger.debug(`Responded to OTA request of '${data.device.name}' with 'NO_IMAGE_AVAILABLE'`); } @@ -182,7 +181,7 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `not_supported`, device: device.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `ota_update`, message: error, meta}), ); @@ -199,7 +198,7 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `checking_if_available`, device: device.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `ota_update`, message: msg, meta}), ); @@ -214,14 +213,14 @@ export default class OTAUpdate extends Extension { if (settings.get().advanced.legacy_api) { const meta = { status: availableResult.available ? 'available' : 'not_available', device: device.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `ota_update`, message: msg, meta}), ); } const payload = this.getEntityPublishPayload(device, availableResult); - this.publishEntityState(device, payload); + await this.publishEntityState(device, payload); this.lastChecked[device.ieeeAddr] = Date.now(); responseData.updateAvailable = availableResult.available; } catch (e) { @@ -231,7 +230,7 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `check_failed`, device: device.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `ota_update`, message: error, meta}), ); @@ -244,14 +243,14 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `update_in_progress`, device: device.name}; - this.mqtt.publish( + await this.mqtt.publish( 'bridge/log', stringify({type: `ota_update`, message: msg, meta}), ); } try { - const onProgress = (progress: number, remaining: number): void => { + const onProgress = async (progress: number, remaining: number): Promise => { let msg = `Update of '${device.name}' at ${progress.toFixed(2)}%`; if (remaining) { msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`; @@ -260,12 +259,12 @@ export default class OTAUpdate extends Extension { logger.info(msg); const payload = this.getEntityPublishPayload(device, 'updating', progress, remaining); - this.publishEntityState(device, payload); + await this.publishEntityState(device, payload); /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `update_progress`, device: device.name, progress}; - this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); } }; @@ -276,7 +275,7 @@ export default class OTAUpdate extends Extension { this.removeProgressAndRemainingFromState(device); const payload = this.getEntityPublishPayload(device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}); - this.publishEntityState(device, payload); + await this.publishEntityState(device, payload); const to = await this.readSoftwareBuildIDAndDateCode(device); const [fromS, toS] = [stringify(from_), stringify(to)]; logger.info(`Device '${device.name}' was updated from '${fromS}' to '${toS}'`); @@ -287,7 +286,7 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `update_succeeded`, device: device.name, from: from_, to}; - this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta})); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta})); } } catch (e) { logger.debug(`Update of '${device.name}' failed (${e})`); @@ -296,12 +295,12 @@ export default class OTAUpdate extends Extension { this.removeProgressAndRemainingFromState(device); const payload = this.getEntityPublishPayload(device, 'available'); - this.publishEntityState(device, payload); + await this.publishEntityState(device, payload); /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `update_failed`, device: device.name}; - this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta})); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta})); } } } diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index ac61412610..a5d60f3f2a 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -85,10 +85,10 @@ export default class Publish extends Extension { } } - legacyLog(payload: KeyValue): void { + async legacyLog(payload: KeyValue): Promise { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - this.mqtt.publish('bridge/log', stringify(payload)); + await this.mqtt.publish('bridge/log', stringify(payload)); } } @@ -133,7 +133,7 @@ export default class Publish extends Extension { const re = this.zigbee.resolveEntity(parsedTopic.ID); if (re == null) { - this.legacyLog({type: `entity_not_found`, message: {friendly_name: parsedTopic.ID}}); + await this.legacyLog({type: `entity_not_found`, message: {friendly_name: parsedTopic.ID}}); logger.error(`Entity '${parsedTopic.ID}' is unknown`); return; } @@ -293,7 +293,7 @@ export default class Publish extends Extension { `Publish '${parsedTopic.type}' '${key}' to '${re.name}' failed: '${error}'`; logger.error(message); logger.debug(error.stack); - this.legacyLog({type: `zigbee_publish_error`, message, meta: {friendly_name: re.name}}); + await this.legacyLog({type: `zigbee_publish_error`, message, meta: {friendly_name: re.name}}); } usedConverters[endpointOrGroupID].push(converter); @@ -301,7 +301,7 @@ export default class Publish extends Extension { for (const [ID, payload] of Object.entries(toPublish)) { if (Object.keys(payload).length != 0) { - this.publishEntityState(toPublishEntity[ID], payload); + await this.publishEntityState(toPublishEntity[ID], payload); } } diff --git a/lib/extension/receive.ts b/lib/extension/receive.ts index 99ede46f28..84d806558d 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -36,8 +36,8 @@ export default class Receive extends Extension { if (!this.debouncers[device.ieeeAddr]) { this.debouncers[device.ieeeAddr] = { payload: {}, - publish: debounce(() => { - this.publishEntityState(device, this.debouncers[device.ieeeAddr].payload, 'publishDebounce'); + publish: debounce(async () => { + await this.publishEntityState(device, this.debouncers[device.ieeeAddr].payload, 'publishDebounce'); this.debouncers[device.ieeeAddr].payload = {}; }, time * 1000), }; @@ -91,7 +91,7 @@ export default class Receive extends Extension { if (!data.device) return; if (!this.shouldProcess(data)) { - utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, + await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState); return; } @@ -106,7 +106,7 @@ export default class Receive extends Extension { if (converters.length == 0 && !ignoreClusters.includes(data.cluster)) { logger.debug(`No converter available for '${data.device.definition.model}' with ` + `cluster '${data.cluster}' and type '${data.type}' and data '${stringify(data.data)}'`); - utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, + await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState); return; } @@ -116,7 +116,7 @@ export default class Receive extends Extension { // - If a payload is returned publish it to the MQTT broker // - If NO payload is returned do nothing. This is for non-standard behaviour // for e.g. click switches where we need to count number of clicks and detect long presses. - const publish = (payload: KeyValue): void => { + const publish = async (payload: KeyValue): Promise => { const options: KeyValue = data.device.options; zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options); @@ -134,7 +134,7 @@ export default class Receive extends Extension { this.publishDebounce(data.device, payload, data.device.options.debounce, data.device.options.debounce_ignore); } else { - this.publishEntityState(data.device, payload); + await this.publishEntityState(data.device, payload); } }; @@ -162,9 +162,9 @@ export default class Receive extends Extension { } if (Object.keys(payload).length) { - publish(payload); + await publish(payload); } else { - utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, + await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState); } } diff --git a/lib/util/utils.ts b/lib/util/utils.ts index faec54aff0..d749e8f63f 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -309,8 +309,8 @@ const hours = (hours: number): number => 1000 * 60 * 60 * hours; const minutes = (minutes: number): number => 1000 * 60 * minutes; const seconds = (seconds: number): number => 1000 * seconds; -function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, allowMessageEmitted: boolean, - publishEntityState: PublishEntityState): void { +async function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, allowMessageEmitted: boolean, + publishEntityState: PublishEntityState): Promise { /** * Prevent 2 MQTT publishes when 1 message event is received; * - In case reason == messageEmitted, receive.ts will only call this when it did not publish a @@ -320,7 +320,7 @@ function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, al */ const allow = data.reason !== 'messageEmitted' || (data.reason === 'messageEmitted' && allowMessageEmitted); if (settings.advanced.last_seen && settings.advanced.last_seen !== 'disable' && allow) { - publishEntityState(data.device, {}, 'lastSeenChanged'); + await publishEntityState(data.device, {}, 'lastSeenChanged'); } } diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 83ede47e83..df15b2363c 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -21,7 +21,7 @@ describe('HomeAssistant extension', () => { await controller.enableDisableExtension(true, 'HomeAssistant'); extension = controller.extensions.find((e) => e.constructor.name === 'HomeAssistant'); if (runTimers) { - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); } } @@ -473,7 +473,7 @@ describe('HomeAssistant extension', () => { await MQTT.events.message(topic1, payload1); await MQTT.events.message(topic2, payload2); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); // Should unsubscribe to not receive all messages that are going to be published to `homeassistant/#` again. expect(MQTT.unsubscribe).toHaveBeenCalledWith(`homeassistant/#`); @@ -1216,7 +1216,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message('homeassistant/status', 'online'); await flushPromises(); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', @@ -1246,7 +1246,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message('hass/status', 'online'); await flushPromises(); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', @@ -1270,7 +1270,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message('hass/status', 'offline'); await flushPromises(); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); }); @@ -1282,7 +1282,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message('hass/status_different', 'offline'); await flushPromises(); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); }); @@ -1367,7 +1367,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({"from": "weather_sensor", "to": "weather_sensor_renamed","homeassistant_rename":true})); await flushPromises(); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); const payload = { @@ -1435,7 +1435,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/rename', stringify({"from": "ha_discovery_group", "to": "ha_discovery_group_new","homeassistant_rename":true})); await flushPromises(); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); const payload = { @@ -2239,7 +2239,7 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 1}, expect.any(Function), ); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); let payload = { @@ -2283,7 +2283,7 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 1}, expect.any(Function), ); - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); await flushPromises(); payload = { @@ -2582,6 +2582,7 @@ describe('HomeAssistant extension', () => { const msg = {data, cluster: 'boschSpecific', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; resetDiscoveryPayloads('0x18fc26000000cafe'); await zigbeeHerdsman.events.message(msg); + await flushPromises(); const payload = { 'availability':[{'topic':'zigbee2mqtt/bridge/state'}], 'command_topic':'zigbee2mqtt/0x18fc26000000cafe/set/device_mode', diff --git a/test/otaUpdate.test.js b/test/otaUpdate.test.js index 093d1e1393..b49b709b0e 100644 --- a/test/otaUpdate.test.js +++ b/test/otaUpdate.test.js @@ -87,10 +87,10 @@ describe('OTA update', () => { expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); - expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190103","softwareBuildID":3}'`); + expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190104","softwareBuildID":4}'`); expect(device.save).toHaveBeenCalledTimes(2); expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': 'immediate'}); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {}); + expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': undefined}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({"update_available":false,"update":{"state":"updating","progress":0}}), @@ -108,7 +108,7 @@ describe('OTA update', () => { ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/update', - stringify({"data":{"from":{"date_code":"20190101","software_build_id":1},"id":"bulb","to":{"date_code":"20190103","software_build_id":3}},"status":"ok"}), + stringify({"data":{"from":{"date_code":"20190101","software_build_id":1},"id":"bulb","to":{"date_code":"20190104","software_build_id":4}},"status":"ok"}), {retain: false, qos: 0}, expect.any(Function) ); expect(MQTT.publish).toHaveBeenCalledWith( @@ -389,10 +389,11 @@ describe('OTA update', () => { expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); - expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190103","softwareBuildID":3}'`); + expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190104","softwareBuildID":4}'`); expect(logger.error).toHaveBeenCalledTimes(0); expect(device.save).toHaveBeenCalledTimes(2); expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': 'immediate'}); + expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': undefined}); }); it('Legacy api: Should handle when OTA update fails', async () => { diff --git a/test/publish.test.js b/test/publish.test.js index 891b30e1b5..352b8c8303 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -58,7 +58,7 @@ describe('Publish', () => { }); afterAll(async () => { - jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); jest.useRealTimers(); sleep.restore(); }); @@ -1486,18 +1486,18 @@ describe('Publish', () => { {retain: false, qos: 0}, expect.any(Function) ); expect(MQTT.publish).toHaveBeenNthCalledWith(3, - 'zigbee2mqtt/group_tradfri_remote', - stringify({"brightness":100,"color_temp":290,"state":"ON","color_mode": "color_temp"}), + 'zigbee2mqtt/ha_discovery_group', + stringify({"brightness":50,"color_mode":"color_temp","color_temp":290,"state":"ON"}), {retain: false, qos: 0}, expect.any(Function) ); expect(MQTT.publish).toHaveBeenNthCalledWith(4, - 'zigbee2mqtt/bulb_2', - stringify({"brightness":100,"color_mode":"color_temp","color_temp":290,"state":"ON"}), + 'zigbee2mqtt/group_tradfri_remote', + stringify({"brightness":100,"color_temp":290,"state":"ON","color_mode": "color_temp"}), {retain: false, qos: 0}, expect.any(Function) ); expect(MQTT.publish).toHaveBeenNthCalledWith(5, - 'zigbee2mqtt/ha_discovery_group', - stringify({"brightness":50,"color_mode":"color_temp","color_temp":290,"state":"ON"}), + 'zigbee2mqtt/bulb_2', + stringify({"brightness":100,"color_mode":"color_temp","color_temp":290,"state":"ON"}), {retain: false, qos: 0}, expect.any(Function) ); expect(MQTT.publish).toHaveBeenNthCalledWith(6,