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:
Koen Kanters
2022-12-18 22:05:16 +01:00
committed by GitHub
parent 825c761c26
commit 01b46cf74e
6 changed files with 63 additions and 44 deletions
+4 -5
View File
@@ -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
View File
@@ -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 */
+5 -2
View File
@@ -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>;
}
}
+2 -2
View File
@@ -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();
+7 -7
View File
@@ -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
View File
@@ -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);