mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-27 21:20:03 +00:00
Implement update_available attribute and discover Home Assistant sensor. #2948
This commit is contained in:
@@ -105,6 +105,15 @@ const cfg = {
|
||||
device_class: 'battery',
|
||||
},
|
||||
},
|
||||
'binary_sensor_update_available': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'update_available',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.update_available}}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_lock': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'lock',
|
||||
@@ -1276,7 +1285,12 @@ class HomeAssistant extends BaseExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
mapping[mappedModel.model].forEach((config) => {
|
||||
const configs = mapping[mappedModel.model].slice();
|
||||
if (mappedModel.hasOwnProperty('ota')) {
|
||||
configs.push(cfg.binary_sensor_update_available);
|
||||
}
|
||||
|
||||
configs.forEach((config) => {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
const payload = {...config.discovery_payload};
|
||||
const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`;
|
||||
|
||||
@@ -3,11 +3,13 @@ const logger = require('../util/logger');
|
||||
const assert = require('assert');
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const MINUTES_10 = 1000 * 60 * 10;
|
||||
|
||||
class OTAUpdate extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.inProgress = new Set();
|
||||
this.lastChecked = {};
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
@@ -15,6 +17,21 @@ class OTAUpdate extends BaseExtension {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
|
||||
}
|
||||
|
||||
async onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
if (data.type !== 'commandQueryNextImageRequest' || !mappedDevice.hasOwnProperty('ota')) return;
|
||||
|
||||
// When a device does a next image request, it will usually do it a few times after each other
|
||||
// with only 10 - 60 seconds inbetween. It doesn' make sense to check for a new update
|
||||
// each time.
|
||||
const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) ?
|
||||
(Date.now() - this.lastChecked[data.device.ieeeAddr]) > MINUTES_10 : true;
|
||||
if (!check || this.inProgress.has(data.device.ieeeAddr)) return;
|
||||
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
const available = await mappedDevice.ota.isUpdateAvailable(data.device, logger, data.data);
|
||||
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
|
||||
}
|
||||
|
||||
async readSoftwareBuildIDAndDateCode(device, update) {
|
||||
try {
|
||||
const endpoint = device.endpoints.find((e) => e.supportsInputCluster('genBasic'));
|
||||
@@ -64,6 +81,8 @@ class OTAUpdate extends BaseExtension {
|
||||
logger.info(message);
|
||||
const meta = {status: available ? 'available' : 'not_available', device: device.name};
|
||||
this.mqtt.log('ota_update', message, meta);
|
||||
this.publishEntityState(device.device.ieeeAddr, {update_available: available});
|
||||
this.lastChecked[device.device.ieeeAddr] = Date.now();
|
||||
} catch (error) {
|
||||
const message = `Failed to check if update available for '${device.name}' (${error.message})`;
|
||||
logger.error(message);
|
||||
@@ -92,6 +111,7 @@ class OTAUpdate extends BaseExtension {
|
||||
logger.info(message);
|
||||
const meta = {status: `update_succeeded`, device: device.name, from: from_, to};
|
||||
this.mqtt.log('ota_update', message, meta);
|
||||
this.publishEntityState(device.device.ieeeAddr, {update_available: false});
|
||||
} catch (error) {
|
||||
const message = `Update of '${device.name}' failed (${error.message})`;
|
||||
logger.error(message);
|
||||
|
||||
@@ -5,7 +5,6 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const Controller = require('../lib/controller');
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
describe('HomeAssistant extension', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -688,4 +687,36 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should discover update_available sensor when device supports it', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
const payload = {
|
||||
"payload_on":true,
|
||||
"payload_off":false,
|
||||
"value_template":"{{ value_json.update_available}}",
|
||||
"state_topic":"zigbee2mqtt/bulb",
|
||||
"json_attributes_topic":"zigbee2mqtt/bulb",
|
||||
"name":"bulb_update_available",
|
||||
"unique_id":"0x000b57fffec6a5b2_update_available_zigbee2mqtt",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x000b57fffec6a5b2"
|
||||
],
|
||||
"name":"bulb",
|
||||
'sw_version': this.version,
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"manufacturer":"IKEA"
|
||||
},
|
||||
"availability_topic":"zigbee2mqtt/bridge/state"
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config',
|
||||
JSON.stringify(payload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,4 +161,21 @@ describe('OTA update', () => {
|
||||
await flushPromises();
|
||||
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
|
||||
});
|
||||
|
||||
it('Should check for update when device requests it', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, logger, {"imageType": 12382});
|
||||
|
||||
// Should not request again when device asks again after a short time
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user