mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2024-11-16 02:18:31 -07:00
Clear Home Assistant MQTT discovery on device remove. #2678
This commit is contained in:
parent
3a463b0782
commit
cb0b0b5af9
@ -1,5 +1,6 @@
|
||||
const MQTT = require('./mqtt');
|
||||
const Zigbee = require('./zigbee');
|
||||
const EventBus = require('./eventBus');
|
||||
const State = require('./state');
|
||||
const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
@ -25,6 +26,7 @@ class Controller {
|
||||
constructor() {
|
||||
this.zigbee = new Zigbee();
|
||||
this.mqtt = new MQTT();
|
||||
this.eventBus = new EventBus();
|
||||
this.state = new State();
|
||||
|
||||
this.publishEntityState = this.publishEntityState.bind(this);
|
||||
@ -32,39 +34,41 @@ class Controller {
|
||||
|
||||
// Initialize extensions.
|
||||
this.extensions = [
|
||||
new ExtensionEntityPublish(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceGroupMembership(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionEntityPublish(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceGroupMembership(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
),
|
||||
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
];
|
||||
|
||||
if (settings.get().advanced.report) {
|
||||
this.extensions.push(new ExtensionDeviceReport(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().homeassistant) {
|
||||
this.extensions.push(new ExtensionHomeAssistant(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (settings.get().advanced.soft_reset_timeout !== 0) {
|
||||
this.extensions.push(new ExtensionSoftReset(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
this.extensions.push(new ExtensionDeviceAvailability(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
20
lib/eventBus.js
Normal file
20
lib/eventBus.js
Normal file
@ -0,0 +1,20 @@
|
||||
const events = require('events');
|
||||
const assert = require('assert');
|
||||
|
||||
const allowedEvents = [
|
||||
'deviceRemoved', // Device has been removed
|
||||
];
|
||||
|
||||
class EventBus extends events.EventEmitter {
|
||||
emit(event, data) {
|
||||
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
|
||||
super.emit(event, data);
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
|
||||
super.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EventBus;
|
@ -6,12 +6,14 @@ class BaseExtension {
|
||||
* @param {MQTT} mqtt MQTT controller
|
||||
* @param {State} state State controller
|
||||
* @param {Function} publishEntityState Method to publish device state to MQTT.
|
||||
* @param {EventBus} eventBus The event bus
|
||||
*/
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,8 +10,8 @@ const configRegex =
|
||||
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
|
||||
|
||||
class BridgeConfig extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
// Bind functions
|
||||
this.permitJoin = this.permitJoin.bind(this);
|
||||
@ -285,6 +285,9 @@ class BridgeConfig extends BaseExtension {
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
// Fire event
|
||||
this.eventBus.emit('deviceRemoved', {device: entity.device});
|
||||
|
||||
// Remove from configuration.yaml
|
||||
settings.removeDevice(entity.settings.ID);
|
||||
|
||||
|
@ -18,8 +18,8 @@ const Hours25 = 1000 * 60 * 60 * 25;
|
||||
* This extensions pings devices to check if they are online.
|
||||
*/
|
||||
class DeviceAvailability extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
this.availability_timeout = settings.get().advanced.availability_timeout;
|
||||
this.timers = {};
|
||||
|
@ -4,8 +4,8 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
class DeviceConfigure extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
this.configuring = new Set();
|
||||
this.attempts = {};
|
||||
|
@ -5,8 +5,8 @@ const debounce = require('debounce');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
class DeviceReceive extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.coordinator = null;
|
||||
this.elapsed = {};
|
||||
this.debouncers = {};
|
||||
|
@ -58,8 +58,8 @@ const pollOnMessage = [
|
||||
];
|
||||
|
||||
class DeviceReport extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.configuring = new Set();
|
||||
this.failed = new Set();
|
||||
this.pollDebouncers = {};
|
||||
|
@ -6,8 +6,8 @@ const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(
|
||||
const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
||||
|
||||
class Groups extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.onStateChange = this.onStateChange.bind(this);
|
||||
}
|
||||
|
||||
|
@ -1144,8 +1144,8 @@ Object.keys(mapping).forEach((key) => {
|
||||
* This extensions handles integration with HomeAssistant
|
||||
*/
|
||||
class HomeAssistant extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
// A map of all discoverd devices
|
||||
this.discovered = {};
|
||||
@ -1160,6 +1160,19 @@ class HomeAssistant extends BaseExtension {
|
||||
|
||||
this.discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
|
||||
this.statusTopic = settings.get().advanced.homeassistant_status_topic;
|
||||
|
||||
this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.device));
|
||||
}
|
||||
|
||||
onDeviceRemoved(device) {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (mappedModel) {
|
||||
logger.info(`Clearing Home Assistant discovery topic for '${device.ieeeAddr}'`);
|
||||
mapping[mappedModel.model].forEach((config) => {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTConnected() {
|
||||
@ -1169,26 +1182,26 @@ class HomeAssistant extends BaseExtension {
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (mappedModel) {
|
||||
this.discover(device.ieeeAddr, mappedModel, true);
|
||||
this.discover(device, mappedModel, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discover(entityID, mappedModel, force=false) {
|
||||
discover(device, mappedModel, force=false) {
|
||||
// Check if already discoverd and check if there are configs.
|
||||
const discover = force || !this.discovered[entityID];
|
||||
const discover = force || !this.discovered[device.ieeeAddr];
|
||||
if (!discover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = settings.getEntity(entityID);
|
||||
const entity = settings.getEntity(device.ieeeAddr);
|
||||
if (!entity || (entity.type === 'device' && !mapping[mappedModel.model]) ||
|
||||
(entity.hasOwnProperty('homeassistant') && !entity.homeassistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapping[mappedModel.model].forEach((config) => {
|
||||
const topic = `${config.type}/${entity.ID}/${config.object_id}/config`;
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
const payload = {...config.discovery_payload};
|
||||
const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`;
|
||||
|
||||
@ -1328,7 +1341,7 @@ class HomeAssistant extends BaseExtension {
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic);
|
||||
});
|
||||
|
||||
this.discovered[entityID] = true;
|
||||
this.discovered[device.ieeeAddr] = true;
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
@ -1353,10 +1366,14 @@ class HomeAssistant extends BaseExtension {
|
||||
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
const device = data.device;
|
||||
if (device && mappedDevice) {
|
||||
this.discover(device.ieeeAddr, mappedDevice);
|
||||
this.discover(device, mappedDevice);
|
||||
}
|
||||
}
|
||||
|
||||
getDiscoveryTopic(config, device) {
|
||||
return `${config.type}/${device.ieeeAddr}/${config.object_id}/config`;
|
||||
}
|
||||
|
||||
// Only for homeassistant.test.js
|
||||
_getMapping() {
|
||||
return mapping;
|
||||
|
@ -5,8 +5,8 @@ const logger = require('../util/logger');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
class NetworkMap extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
// Subscribe to topic.
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
|
||||
|
@ -9,8 +9,8 @@ const BaseExtension = require('./baseExtension');
|
||||
* This extensions soft resets the ZNP after a certain timeout.
|
||||
*/
|
||||
class SoftReset extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.timer = null;
|
||||
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ describe('HomeAssistant extension', () => {
|
||||
it('Should have mapping for all devices supported by zigbee-herdsman-converters', () => {
|
||||
const missing = [];
|
||||
const HomeAssistant = require('../lib/extension/homeassistant');
|
||||
const ha = new HomeAssistant(null, null, null, null);
|
||||
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});
|
||||
|
||||
require('zigbee-herdsman-converters').devices.forEach((d) => {
|
||||
if (!ha._getMapping()[d.model]) {
|
||||
@ -637,4 +637,54 @@ describe('HomeAssistant extension', () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should clear discovery when device is removed', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'weather_sensor');
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/temperature/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/humidity/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/pressure/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/battery/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/linkquality/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not clear discovery when unsupported device is removed', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'unsupported2');
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user