mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-27 21:20:03 +00:00
Use fileVersion for Home Assistant update sensor instead of softwareBuildID (#15630)
* Use fileVersion for Home Assistant update sensor instead of softwareBuildID. https://github.com/Koenkk/zigbee2mqtt/issues/15502 * Remove onlythis
This commit is contained in:
@@ -1058,7 +1058,7 @@ export default class HomeAssistant extends Extension {
|
||||
const updateStateSensor: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: 'update_state',
|
||||
mockProperties: [{property: 'update', value: {state: null}}],
|
||||
mockProperties: [], // update is mocked below with updateSensor
|
||||
discovery_payload: {
|
||||
icon: 'mdi:update',
|
||||
value_template: `{{ value_json['update']['state'] }}`,
|
||||
@@ -1094,8 +1094,7 @@ export default class HomeAssistant extends Extension {
|
||||
command_topic: `${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/update`,
|
||||
payload_install: `{"id": "${entity.ieeeAddr}"}`,
|
||||
value_template: `{{ value_json['update']['installed_version'] }}`,
|
||||
latest_version_template: `{% if value_json['update']['state'] == "available" %}{{ 'newer' }}` +
|
||||
`{% else %}{{ value_json['update']['installed_version'] }}{% endif %}`,
|
||||
latest_version_template: `{{ value_json['update']['latest_version'] }}`,
|
||||
},
|
||||
};
|
||||
configs.push(updateSensor);
|
||||
@@ -1461,8 +1460,8 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.isDevice() && entity.definition?.ota && message.hasOwnProperty('update')) {
|
||||
message['update']['installed_version'] = entity.zh.softwareBuildID || 'unknown';
|
||||
if (entity.isDevice() && entity.definition?.ota && !message.update?.hasOwnProperty('installed_version')) {
|
||||
message.update = {...message.update, installed_version: -1, latest_version: -1};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-17
@@ -24,8 +24,13 @@ function isValidUrl(url: string): boolean {
|
||||
|
||||
type UpdateState = 'updating' | 'idle' | 'available';
|
||||
interface UpdatePayload {
|
||||
update_available?: boolean
|
||||
// eslint-disable-next-line camelcase
|
||||
update: {progress?: number, remaining?: number, state: UpdateState}, update_available?: boolean}
|
||||
update: {
|
||||
progress?: number, remaining?: number, state: UpdateState,
|
||||
installed_version: number | null, latest_version: number | null
|
||||
}
|
||||
}
|
||||
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
|
||||
const topicRegex =
|
||||
@@ -86,18 +91,18 @@ export default class OTAUpdate extends Extension {
|
||||
if (!check) return;
|
||||
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
let available = false;
|
||||
let availableResult: zhc.OtaUpdateAvailableResult = null;
|
||||
try {
|
||||
available = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
|
||||
availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
|
||||
} catch (e) {
|
||||
supportsOTA = false;
|
||||
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
|
||||
}
|
||||
|
||||
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
|
||||
const payload = this.getEntityPublishPayload(data.device, availableResult ?? 'idle');
|
||||
this.publishEntityState(data.device, payload);
|
||||
|
||||
if (available) {
|
||||
if (availableResult?.available) {
|
||||
const message = `Update available for '${data.device.name}'`;
|
||||
logger.info(message);
|
||||
|
||||
@@ -131,14 +136,22 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private getEntityPublishPayload(state: UpdateState, progress: number=null, remaining: number=null): UpdatePayload {
|
||||
const payload: UpdatePayload = {update: {state}};
|
||||
private getEntityPublishPayload(device: Device, state: zhc.OtaUpdateAvailableResult | UpdateState,
|
||||
progress: number=null, remaining: number=null): UpdatePayload {
|
||||
const deviceUpdateState = this.state.get(device).update;
|
||||
const payload: UpdatePayload = {update: {
|
||||
state: typeof state === 'string' ? state : (state.available ? 'available' : 'idle'),
|
||||
installed_version: typeof state === 'string' ?
|
||||
deviceUpdateState?.installed_version : state.currentFileVersion,
|
||||
latest_version: typeof state === 'string' ?
|
||||
deviceUpdateState?.latest_version : state.otaFileVersion,
|
||||
}};
|
||||
if (progress !== null) payload.update.progress = progress;
|
||||
if (remaining !== null) payload.update.remaining = Math.round(remaining);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
payload.update_available = state === 'available';
|
||||
payload.update_available = typeof state === 'string' ? state === 'available' : state.available;
|
||||
}
|
||||
|
||||
return payload;
|
||||
@@ -189,23 +202,24 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
try {
|
||||
const available = await device.definition.ota.isUpdateAvailable(device.zh, logger);
|
||||
const msg = `${available ? 'Update' : 'No update'} available for '${device.name}'`;
|
||||
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, logger);
|
||||
const msg = `${availableResult.available ? 'Update' : 'No update'} available for '${device.name}'`;
|
||||
logger.info(msg);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: available ? 'available' : 'not_available', device: device.name};
|
||||
const meta = {
|
||||
status: availableResult.available ? 'available' : 'not_available', device: device.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
stringify({type: `ota_update`, message: msg, meta}),
|
||||
);
|
||||
}
|
||||
|
||||
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
|
||||
const payload = this.getEntityPublishPayload(device, availableResult);
|
||||
this.publishEntityState(device, payload);
|
||||
this.lastChecked[device.ieeeAddr] = Date.now();
|
||||
responseData.updateAvailable = available;
|
||||
responseData.updateAvailable = availableResult.available;
|
||||
} catch (e) {
|
||||
error = `Failed to check if update available for '${device.name}' (${e.message})`;
|
||||
errorStack = e.stack;
|
||||
@@ -241,7 +255,7 @@ export default class OTAUpdate extends Extension {
|
||||
|
||||
logger.info(msg);
|
||||
|
||||
const payload = this.getEntityPublishPayload('updating', progress, remaining);
|
||||
const payload = this.getEntityPublishPayload(device, 'updating', progress, remaining);
|
||||
this.publishEntityState(device, payload);
|
||||
|
||||
/* istanbul ignore else */
|
||||
@@ -252,11 +266,12 @@ export default class OTAUpdate extends Extension {
|
||||
};
|
||||
|
||||
const from_ = await this.readSoftwareBuildIDAndDateCode(device, 'immediate');
|
||||
await device.definition.ota.updateToLatest(device.zh, logger, onProgress);
|
||||
const fileVersion = await device.definition.ota.updateToLatest(device.zh, logger, onProgress);
|
||||
logger.info(`Finished update of '${device.name}'`);
|
||||
this.eventBus.emitReconfigure({device});
|
||||
this.removeProgressAndRemainingFromState(device);
|
||||
const payload = this.getEntityPublishPayload('idle');
|
||||
const payload = this.getEntityPublishPayload(device,
|
||||
{available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion});
|
||||
this.publishEntityState(device, payload);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(device, 'active');
|
||||
const [fromS, toS] = [stringify(from_), stringify(to)];
|
||||
@@ -276,7 +291,7 @@ export default class OTAUpdate extends Extension {
|
||||
errorStack = e.stack;
|
||||
|
||||
this.removeProgressAndRemainingFromState(device);
|
||||
const payload = this.getEntityPublishPayload('available');
|
||||
const payload = this.getEntityPublishPayload(device, 'available');
|
||||
this.publishEntityState(device, payload);
|
||||
|
||||
/* istanbul ignore else */
|
||||
|
||||
Vendored
+5
-2
@@ -104,6 +104,8 @@ declare global {
|
||||
access: number, property: string, unit?: string,
|
||||
value_min?: number, value_max?: number}
|
||||
|
||||
interface OtaUpdateAvailableResult {available: boolean, currentFileVersion: number, otaFileVersion: number}
|
||||
|
||||
interface Definition {
|
||||
model: string,
|
||||
zigbeeModel: string[],
|
||||
@@ -120,9 +122,10 @@ declare global {
|
||||
onEvent?: (type: string, data: KeyValue, device: zh.Device,
|
||||
settings: KeyValue, state: KeyValue) => Promise<void>;
|
||||
ota?: {
|
||||
isUpdateAvailable: (device: zh.Device, logger: Logger, data?: KeyValue) => Promise<boolean>;
|
||||
isUpdateAvailable: (device: zh.Device, logger: Logger, data?: KeyValue)
|
||||
=> Promise<OtaUpdateAvailableResult>;
|
||||
updateToLatest: (device: zh.Device, logger: Logger,
|
||||
onProgress: (progress: number, remaining: number) => void) => Promise<void>;
|
||||
onProgress: (progress: number, remaining: number) => void) => Promise<number>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('Frontend', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: "unknown"}}),
|
||||
stringify({state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -208,7 +208,7 @@ describe('Frontend', () => {
|
||||
|
||||
// Received message on socket
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1);
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: "unknown"}}}));
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}}));
|
||||
|
||||
// Shouldnt set when not ready
|
||||
mockWSClient.implementation.send.mockClear();
|
||||
|
||||
@@ -956,7 +956,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": "unknown"}}),
|
||||
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -972,7 +972,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": "unknown"}}),
|
||||
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -988,7 +988,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"linkquality": null,"state": "ON", "update_available": null, "power_on_behavior": null, "update": {"state": null, "installed_version": "unknown"}}),
|
||||
stringify({"linkquality": null,"state": "ON", "update_available": null, "power_on_behavior": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -1076,13 +1076,13 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": "unknown"}}),
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote',
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": "unknown"}}),
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -1106,13 +1106,13 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": "unknown"}}),
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote',
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": "unknown"}}),
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
+13
-11
@@ -72,6 +72,7 @@ describe('OTA update', () => {
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
onUpdate(0, null);
|
||||
onUpdate(10, 3600.2123);
|
||||
return 90;
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
|
||||
@@ -99,7 +100,7 @@ describe('OTA update', () => {
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":false,"update":{"state":"idle"}}),
|
||||
stringify({"update_available":false,"update":{"state":"idle","installed_version":90,"latest_version":90}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -145,7 +146,7 @@ describe('OTA update', () => {
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
@@ -157,7 +158,7 @@ describe('OTA update', () => {
|
||||
);
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
|
||||
@@ -250,7 +251,7 @@ describe('OTA update', () => {
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@@ -267,13 +268,13 @@ describe('OTA update', () => {
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":true,"update":{"state":"available"}}),
|
||||
stringify({"update_available":true,"update":{"state":"available","installed_version":10,"latest_version":12}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
@@ -306,7 +307,7 @@ describe('OTA update', () => {
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@@ -317,7 +318,7 @@ describe('OTA update', () => {
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":false,"update":{"state":"idle"}}),
|
||||
stringify({"update_available":false,"update":{"state":"idle","installed_version": 13, "latest_version": 13}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
@@ -328,7 +329,7 @@ describe('OTA update', () => {
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@@ -373,6 +374,7 @@ describe('OTA update', () => {
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
onUpdate(0, null);
|
||||
onUpdate(10, 3600);
|
||||
return 91;
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
@@ -415,7 +417,7 @@ describe('OTA update', () => {
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
@@ -423,7 +425,7 @@ describe('OTA update', () => {
|
||||
expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 13, otaFileVersion: 15});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
|
||||
|
||||
Reference in New Issue
Block a user