fix: Enforce no floating promises (#22880)

* Enforce no floating promises.

* forEach => for/of. Fix tests.
This commit is contained in:
Nerivec authored and GitHub committed 2024-06-04 20:05:39 +02:00
1 parent dbf0a3358d
commit 2b8eaa196a
22 files changed
+231 -231

No files matched your search

+4 -2
View File
@@ -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',
},
}],
};
+1 -13
View File
@@ -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
+2 -2
View File
@@ -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');
}
}
}
+16 -16
View File
@@ -60,9 +60,9 @@ export default class Availability extends Extension {
}
}
private addToPingQueue(device: Device): void {
private async addToPingQueue(device: Device): Promise<void> {
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<void> {
@@ -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<void> {
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<void> {
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<void> {
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);
}
}
+3 -3
View File
@@ -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}}),
);
+36 -36
View File
@@ -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<void> =>
const publishEvent = async (type: string, data: KeyValue): Promise<void> =>
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<void> {
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) {
+3 -3
View File
@@ -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'));
+7 -8
View File
@@ -16,7 +16,7 @@ export default class ExternalExtension extends Extension {
override async start(): Promise<void> {
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<void> {
for (const extension of this.getListOfUserDefinedExtensions()) {
await this.loadExtension(utils.loadModuleFromText(extension.code, extension.name) as typeof Extension);
}
}
private async publishExtensions(): Promise<void> {
+1 -1
View File
@@ -87,7 +87,7 @@ export default class Frontend extends Extension {
}
override async stop(): Promise<void> {
super.stop();
await super.stop();
this.wss?.clients.forEach((client) => {
client.send(stringify({topic: 'bridge/state', payload: 'offline'}));
client.terminate();
+10 -10
View File
@@ -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<ParsedMQTTMessage> {
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<void> {
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}),
);
+47 -37
View File
@@ -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<void> {
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<void> {
await this.discover(data.group);
}
@bind async onPublishEntityState(data: eventdata.PublishEntityState): Promise<void> {
@@ -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<void> {
// 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<string> = 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<void> {
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<void> {
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 {
+36 -36
View File
@@ -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<void> {
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<void> {
await this.zigbee.permitJoin(message.toLowerCase() === 'true');
this.publish();
await this.publish();
}
@bind async reset(): Promise<void> {
@@ -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<void> {
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<void> {
@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> => {
// 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<void> {
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<void> {
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'}}),
);
@@ -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});
}
}
+1 -1
View File
@@ -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, {});
}
}
+5 -5
View File
@@ -7,7 +7,7 @@ import Extension from './extension';
export default class OnEvent extends Extension {
override async start(): Promise<void> {
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<void> {
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<void> {
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;
+19 -20
View File
@@ -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<void> => {
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}));
}
}
}
+5 -5
View File
@@ -85,10 +85,10 @@ export default class Publish extends Extension {
}
}
legacyLog(payload: KeyValue): void {
async legacyLog(payload: KeyValue): Promise<void> {
/* 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);
}
}
+8 -8
View File
@@ -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<void> => {
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);
}
}
+3 -3
View File
@@ -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<void> {
/**
* 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');
}
}
+11 -10
View File
@@ -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',
+5 -4
View File
@@ -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 () => {
+7 -7
View File
@@ -58,7 +58,7 @@ describe('Publish', () => {
});
afterAll(async () => {
jest.runOnlyPendingTimers();
await jest.runOnlyPendingTimersAsync();
jest.useRealTimers();
sleep.restore();
});
@@ -1486,20 +1486,20 @@ describe('Publish', () => {
{retain: false, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenNthCalledWith(3,
'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/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(4,
expect(MQTT.publish).toHaveBeenNthCalledWith(5,
'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(5,
'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(6,
'zigbee2mqtt/group_with_tradfri',
stringify({"brightness":100,"color_mode":"color_temp","color_temp":290,"state":"ON"}),