2020-05-04 11:06:50 -07:00
|
|
|
const logger = require('../util/logger');
|
|
|
|
const utils = require('../util/utils');
|
|
|
|
const Extension = require('./extension');
|
|
|
|
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
|
|
|
const settings = require('../util/settings');
|
2020-07-28 13:12:22 -07:00
|
|
|
const Transport = require('winston-transport');
|
2020-09-24 09:06:43 -07:00
|
|
|
const stringify = require('json-stable-stringify-without-jsonify');
|
2020-09-04 09:42:24 -07:00
|
|
|
const objectAssignDeep = require(`object-assign-deep`);
|
2021-02-06 08:32:20 -07:00
|
|
|
const {updatedDiff, addedDiff} = require('deep-object-diff');
|
2020-05-04 11:06:50 -07:00
|
|
|
|
|
|
|
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
|
|
|
|
2020-05-24 09:16:39 -07:00
|
|
|
class Bridge extends Extension {
|
2021-02-06 08:32:20 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback) {
|
2020-05-04 11:06:50 -07:00
|
|
|
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
2020-07-29 14:10:03 -07:00
|
|
|
this.enableDisableExtension = enableDisableExtension;
|
2021-02-06 08:32:20 -07:00
|
|
|
this.restartCallback = restartCallback;
|
2020-06-13 10:35:09 -07:00
|
|
|
this.lastJoinedDeviceIeeeAddr = null;
|
2020-07-28 13:12:22 -07:00
|
|
|
this.setupMQTTLogging();
|
2021-02-06 08:32:20 -07:00
|
|
|
this.restartRequired = false;
|
2020-05-04 11:06:50 -07:00
|
|
|
|
|
|
|
this.requestLookup = {
|
2020-06-13 15:28:24 -07:00
|
|
|
'device/options': this.deviceOptions.bind(this),
|
2020-12-28 14:57:35 -07:00
|
|
|
'device/configure_reporting': this.deviceConfigureReporting.bind(this),
|
2020-06-13 08:22:00 -07:00
|
|
|
'device/remove': this.deviceRemove.bind(this),
|
2020-06-13 10:35:09 -07:00
|
|
|
'device/rename': this.deviceRename.bind(this),
|
2020-06-13 14:42:58 -07:00
|
|
|
'group/add': this.groupAdd.bind(this),
|
2020-06-13 15:28:24 -07:00
|
|
|
'group/options': this.groupOptions.bind(this),
|
2020-06-13 14:28:06 -07:00
|
|
|
'group/remove': this.groupRemove.bind(this),
|
2020-06-13 10:35:09 -07:00
|
|
|
'group/rename': this.groupRename.bind(this),
|
2020-07-13 14:00:33 -07:00
|
|
|
'permit_join': this.permitJoin.bind(this),
|
2021-02-06 08:32:20 -07:00
|
|
|
'restart': this.restart.bind(this),
|
2020-07-13 14:00:33 -07:00
|
|
|
'touchlink/factory_reset': this.touchlinkFactoryReset.bind(this),
|
2020-09-09 09:24:32 -07:00
|
|
|
'touchlink/identify': this.touchlinkIdentify.bind(this),
|
2020-09-08 11:24:49 -07:00
|
|
|
'touchlink/scan': this.touchlinkScan.bind(this),
|
2020-07-21 12:14:39 -07:00
|
|
|
'health_check': this.healthCheck.bind(this),
|
2021-01-30 07:44:36 -07:00
|
|
|
'options': this.bridgeOptions.bind(this),
|
2021-02-06 08:32:20 -07:00
|
|
|
// Below are deprecated
|
|
|
|
'config/last_seen': this.configLastSeen.bind(this),
|
|
|
|
'config/homeassistant': this.configHomeAssistant.bind(this),
|
|
|
|
'config/elapsed': this.configElapsed.bind(this),
|
|
|
|
'config/log_level': this.configLogLevel.bind(this),
|
2020-05-04 11:06:50 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async onMQTTConnected() {
|
2020-05-24 09:16:39 -07:00
|
|
|
this.zigbee2mqttVersion = await utils.getZigbee2mqttVersion();
|
|
|
|
this.coordinatorVersion = await this.zigbee.getCoordinatorVersion();
|
|
|
|
|
2020-07-13 12:45:44 -07:00
|
|
|
this.eventBus.on(`groupMembersChanged`, () => this.publishGroups());
|
2021-01-17 01:51:32 -07:00
|
|
|
this.eventBus.on(`devicesChanged`, () => {
|
|
|
|
this.publishDevices();
|
|
|
|
this.publishInfo();
|
|
|
|
});
|
|
|
|
this.eventBus.on(`deviceRenamed`, () => {
|
|
|
|
this.publishInfo();
|
|
|
|
});
|
|
|
|
this.eventBus.on(`groupRenamed`, () => {
|
|
|
|
this.publishInfo();
|
|
|
|
});
|
2021-01-03 03:08:33 -07:00
|
|
|
this.zigbee.on('permitJoinChanged', (data) => this.permitJoinChanged(data));
|
2020-05-04 11:06:50 -07:00
|
|
|
await this.publishInfo();
|
|
|
|
await this.publishDevices();
|
|
|
|
await this.publishGroups();
|
|
|
|
}
|
|
|
|
|
2020-07-28 13:12:22 -07:00
|
|
|
setupMQTTLogging() {
|
|
|
|
const mqtt = this.mqtt;
|
|
|
|
class EventTransport extends Transport {
|
|
|
|
log(info, callback) {
|
2020-08-13 11:00:35 -07:00
|
|
|
const payload = stringify({message: info.message, level: info.level});
|
2020-07-28 13:12:22 -07:00
|
|
|
mqtt.publish(`bridge/logging`, payload, {}, settings.get().mqtt.base_topic, true);
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-19 10:14:40 -07:00
|
|
|
logger.addTransport(new EventTransport());
|
2020-07-28 13:12:22 -07:00
|
|
|
}
|
|
|
|
|
2021-01-03 03:08:33 -07:00
|
|
|
permitJoinChanged(data) {
|
2021-02-06 08:32:20 -07:00
|
|
|
if (!this.zigbee.isStopping()) {
|
|
|
|
this.publishInfo();
|
|
|
|
}
|
2021-01-03 03:08:33 -07:00
|
|
|
}
|
|
|
|
|
2020-05-04 11:06:50 -07:00
|
|
|
async onMQTTMessage(topic, message) {
|
|
|
|
const match = topic.match(requestRegex);
|
|
|
|
if (match && this.requestLookup[match[1].toLowerCase()]) {
|
2020-06-15 11:10:30 -07:00
|
|
|
message = utils.parseJSON(message, message);
|
2020-06-13 14:46:04 -07:00
|
|
|
|
2020-05-07 10:41:03 -07:00
|
|
|
try {
|
|
|
|
const response = await this.requestLookup[match[1].toLowerCase()](message);
|
2020-08-13 11:00:35 -07:00
|
|
|
await this.mqtt.publish(`bridge/response/${match[1]}`, stringify(response));
|
2020-05-07 10:41:03 -07:00
|
|
|
} catch (error) {
|
|
|
|
logger.error(`Request '${topic}' failed with error: '${error.message}'`);
|
|
|
|
const response = utils.getResponse(message, {}, error.message);
|
2020-08-13 11:00:35 -07:00
|
|
|
await this.mqtt.publish(`bridge/response/${match[1]}`, stringify(response));
|
2020-05-07 10:41:03 -07:00
|
|
|
}
|
2020-05-04 11:06:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async onZigbeeEvent(type, data, resolvedEntity) {
|
2020-06-13 10:35:09 -07:00
|
|
|
if (type === 'deviceJoined' && resolvedEntity) {
|
|
|
|
this.lastJoinedDeviceIeeeAddr = resolvedEntity.device.ieeeAddr;
|
|
|
|
}
|
|
|
|
|
2020-08-20 11:55:40 -07:00
|
|
|
if (['deviceJoined', 'deviceLeave', 'deviceInterview', 'deviceAnnounce'].includes(type)) {
|
2020-05-04 11:06:50 -07:00
|
|
|
let payload;
|
|
|
|
const ieeeAddress = data.device ? data.device.ieeeAddr : data.ieeeAddr;
|
2020-07-13 14:00:33 -07:00
|
|
|
if (type === 'deviceJoined') {
|
|
|
|
payload = {friendly_name: resolvedEntity.settings.friendlyName, ieee_address: ieeeAddress};
|
|
|
|
} else if (type === 'deviceInterview') {
|
|
|
|
payload = {
|
|
|
|
friendly_name: resolvedEntity.settings.friendlyName, status: data.status, ieee_address: ieeeAddress,
|
|
|
|
};
|
2020-05-04 11:06:50 -07:00
|
|
|
if (data.status === 'successful') {
|
2020-10-01 09:33:59 -07:00
|
|
|
const definition = resolvedEntity.definition;
|
|
|
|
payload.supported = !!definition;
|
|
|
|
payload.definition = this.getDefinitionPayload(definition);
|
2020-05-04 11:06:50 -07:00
|
|
|
}
|
2020-08-20 11:55:40 -07:00
|
|
|
} else if (type === 'deviceAnnounce') {
|
|
|
|
payload = {
|
|
|
|
friendly_name: resolvedEntity.settings.friendlyName, ieee_address: ieeeAddress,
|
|
|
|
};
|
2020-07-13 14:00:33 -07:00
|
|
|
} else payload = {ieee_address: ieeeAddress}; // deviceLeave
|
2020-05-04 11:06:50 -07:00
|
|
|
|
2020-07-13 14:00:33 -07:00
|
|
|
await this.mqtt.publish(
|
|
|
|
'bridge/event',
|
2020-08-13 11:00:35 -07:00
|
|
|
stringify({type: utils.toSnakeCase(type), data: payload}),
|
2020-07-13 14:00:33 -07:00
|
|
|
{retain: false, qos: 0},
|
|
|
|
);
|
2020-05-04 11:06:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if ('deviceLeave' === type || ('deviceInterview' === type && data.status !== 'started')) {
|
|
|
|
await this.publishDevices();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-07 10:41:03 -07:00
|
|
|
/**
|
|
|
|
* Requests
|
|
|
|
*/
|
|
|
|
|
2020-06-13 15:28:24 -07:00
|
|
|
async deviceOptions(message) {
|
2020-06-13 14:28:06 -07:00
|
|
|
return this.changeEntityOptions('device', message);
|
|
|
|
}
|
|
|
|
|
2020-06-13 15:28:24 -07:00
|
|
|
async groupOptions(message) {
|
2020-06-13 14:28:06 -07:00
|
|
|
return this.changeEntityOptions('group', message);
|
|
|
|
}
|
|
|
|
|
2021-01-30 07:44:36 -07:00
|
|
|
async bridgeOptions(message) {
|
2021-02-06 08:32:20 -07:00
|
|
|
if (typeof message !== 'object' || typeof message.options !== 'object') {
|
|
|
|
throw new Error(`Invalid payload`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const diffUpdated = updatedDiff(settings.get(), message.options);
|
|
|
|
const diffAdded = addedDiff(settings.get(), message.options);
|
|
|
|
const newSettings = objectAssignDeep.noMutate(diffUpdated, diffAdded);
|
|
|
|
|
|
|
|
// deep-object-diff converts arrays to objects, set original array back here
|
|
|
|
const convertBackArray = (before, after) => {
|
|
|
|
for (const [key, afterValue] of Object.entries(after)) {
|
|
|
|
const beforeValue = before[key];
|
|
|
|
if (Array.isArray(beforeValue)) {
|
|
|
|
after[key] = beforeValue;
|
2021-02-07 06:07:27 -07:00
|
|
|
} else if (afterValue && typeof beforeValue === 'object') {
|
2021-02-06 08:32:20 -07:00
|
|
|
convertBackArray(beforeValue, afterValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
convertBackArray(message.options, newSettings);
|
|
|
|
|
|
|
|
const restartRequired = settings.apply(newSettings);
|
|
|
|
if (restartRequired) this.restartRequired = true;
|
|
|
|
|
|
|
|
// Apply some settings on-the-fly.
|
|
|
|
if (newSettings.hasOwnProperty('permit_join')) {
|
|
|
|
await this.zigbee.permitJoin(newSettings.permit_join);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newSettings.hasOwnProperty('homeassistant')) {
|
|
|
|
this.enableDisableExtension(newSettings.homeassistant, 'HomeAssistant');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newSettings.hasOwnProperty('advanced') && newSettings.advanced.hasOwnProperty('log_level')) {
|
|
|
|
logger.setLevel(newSettings.advanced.log_level);
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info('Succesfully changed options');
|
|
|
|
this.publishInfo();
|
|
|
|
return utils.getResponse(message, {restart_required: this.restartRequired}, null);
|
2021-01-30 07:44:36 -07:00
|
|
|
}
|
|
|
|
|
2020-06-13 08:22:00 -07:00
|
|
|
async deviceRemove(message) {
|
|
|
|
return this.removeEntity('device', message);
|
|
|
|
}
|
2020-05-07 10:41:03 -07:00
|
|
|
|
2020-06-13 08:22:00 -07:00
|
|
|
async groupRemove(message) {
|
|
|
|
return this.removeEntity('group', message);
|
|
|
|
}
|
2020-05-07 10:41:03 -07:00
|
|
|
|
2020-07-21 12:14:39 -07:00
|
|
|
async healthCheck(message) {
|
|
|
|
return utils.getResponse(message, {healthy: true}, null);
|
|
|
|
}
|
|
|
|
|
2020-06-13 14:42:58 -07:00
|
|
|
async groupAdd(message) {
|
2020-07-13 14:00:33 -07:00
|
|
|
if (typeof message === 'object' && !message.hasOwnProperty('friendly_name')) {
|
2020-06-13 14:42:58 -07:00
|
|
|
throw new Error(`Invalid payload`);
|
|
|
|
}
|
|
|
|
|
2020-07-13 14:00:33 -07:00
|
|
|
const friendlyName = typeof message === 'object' ? message.friendly_name : message;
|
2020-07-15 13:16:18 -07:00
|
|
|
const ID = typeof message === 'object' && message.hasOwnProperty('id') ? message.id : null;
|
2020-06-13 14:42:58 -07:00
|
|
|
const group = settings.addGroup(friendlyName, ID);
|
|
|
|
this.zigbee.createGroup(group.ID);
|
|
|
|
this.publishGroups();
|
2020-07-15 13:16:18 -07:00
|
|
|
return utils.getResponse(message, {friendly_name: group.friendlyName, id: group.ID}, null);
|
2020-06-13 14:42:58 -07:00
|
|
|
}
|
|
|
|
|
2020-06-13 10:35:09 -07:00
|
|
|
async deviceRename(message) {
|
|
|
|
return this.renameEntity('device', message);
|
|
|
|
}
|
|
|
|
|
|
|
|
async groupRename(message) {
|
|
|
|
return this.renameEntity('group', message);
|
|
|
|
}
|
|
|
|
|
2021-02-06 08:32:20 -07:00
|
|
|
async restart(message) {
|
|
|
|
// Wait 500 ms before restarting so response can be send.
|
|
|
|
setTimeout(this.restartCallback, 500);
|
|
|
|
logger.info('Restarting Zigbee2MQTT');
|
|
|
|
return utils.getResponse(message, {}, null);
|
|
|
|
}
|
|
|
|
|
2020-05-24 09:16:39 -07:00
|
|
|
async permitJoin(message) {
|
2020-10-11 06:05:21 -07:00
|
|
|
if (typeof message === 'object' && !message.hasOwnProperty('value')) {
|
|
|
|
throw new Error('Invalid payload');
|
|
|
|
}
|
|
|
|
|
|
|
|
let value;
|
2021-01-03 03:08:33 -07:00
|
|
|
let time;
|
2020-10-11 06:05:21 -07:00
|
|
|
let resolvedEntity;
|
|
|
|
if (typeof message === 'object') {
|
|
|
|
value = message.value;
|
2021-01-03 03:08:33 -07:00
|
|
|
time = message.time;
|
2020-10-11 06:05:21 -07:00
|
|
|
if (message.device) {
|
|
|
|
resolvedEntity = this.zigbee.resolveEntity(message.device);
|
|
|
|
if (!resolvedEntity || resolvedEntity.type !== 'device') {
|
|
|
|
throw new Error(`Device '${message.device}' does not exist`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
value = message;
|
|
|
|
}
|
|
|
|
|
2021-01-10 08:54:01 -07:00
|
|
|
if (typeof value === 'string') {
|
|
|
|
value = value.toLowerCase() === 'true';
|
|
|
|
}
|
|
|
|
|
2021-01-03 03:08:33 -07:00
|
|
|
await this.zigbee.permitJoin(value, resolvedEntity, time);
|
|
|
|
const response = {value};
|
|
|
|
if (resolvedEntity) response.device = message.device;
|
|
|
|
if (time) response.time = message.time;
|
|
|
|
return utils.getResponse(message, response, null);
|
2020-05-07 10:41:03 -07:00
|
|
|
}
|
|
|
|
|
2021-02-06 08:32:20 -07:00
|
|
|
// Deprecated
|
2020-06-15 09:58:32 -07:00
|
|
|
configLastSeen(message) {
|
|
|
|
const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local'];
|
|
|
|
const value = this.getValue(message);
|
|
|
|
if (!allowed.includes(value)) {
|
|
|
|
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:15:08 -07:00
|
|
|
settings.set(['advanced', 'last_seen'], value);
|
2020-09-04 09:42:24 -07:00
|
|
|
this.publishInfo();
|
2020-06-15 09:58:32 -07:00
|
|
|
return utils.getResponse(message, {value}, null);
|
|
|
|
}
|
|
|
|
|
2021-02-06 08:32:20 -07:00
|
|
|
// Deprecated
|
2020-07-29 14:10:03 -07:00
|
|
|
configHomeAssistant(message) {
|
|
|
|
const allowed = [true, false];
|
|
|
|
const value = this.getValue(message);
|
|
|
|
if (!allowed.includes(value)) {
|
|
|
|
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.enableDisableExtension(value, 'HomeAssistant');
|
|
|
|
settings.set(['homeassistant'], value);
|
2020-09-04 09:42:24 -07:00
|
|
|
this.publishInfo();
|
2020-07-29 14:10:03 -07:00
|
|
|
return utils.getResponse(message, {value}, null);
|
|
|
|
}
|
|
|
|
|
2021-02-06 08:32:20 -07:00
|
|
|
// Deprecated
|
2020-06-15 09:58:32 -07:00
|
|
|
configElapsed(message) {
|
|
|
|
const allowed = [true, false];
|
|
|
|
const value = this.getValue(message);
|
|
|
|
if (!allowed.includes(value)) {
|
|
|
|
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
settings.set(['advanced', 'elapsed'], value);
|
2020-09-04 09:42:24 -07:00
|
|
|
this.publishInfo();
|
2020-06-15 09:58:32 -07:00
|
|
|
return utils.getResponse(message, {value}, null);
|
|
|
|
}
|
|
|
|
|
2021-02-06 08:32:20 -07:00
|
|
|
// Deprecated
|
2020-06-15 09:58:32 -07:00
|
|
|
configLogLevel(message) {
|
|
|
|
const allowed = ['error', 'warn', 'info', 'debug'];
|
|
|
|
const value = this.getValue(message);
|
|
|
|
if (!allowed.includes(value)) {
|
|
|
|
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.setLevel(value);
|
2020-06-15 10:06:08 -07:00
|
|
|
this.publishInfo();
|
2020-06-15 09:58:32 -07:00
|
|
|
return utils.getResponse(message, {value}, null);
|
|
|
|
}
|
|
|
|
|
2020-09-09 09:24:32 -07:00
|
|
|
async touchlinkIdentify(message) {
|
|
|
|
if (typeof message !== 'object' || !message.hasOwnProperty('ieee_address') ||
|
|
|
|
!message.hasOwnProperty('channel')) {
|
|
|
|
throw new Error('Invalid payload');
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info(`Start Touchlink identify of '${message.ieee_address}' on channel ${message.channel}`);
|
|
|
|
await this.zigbee.touchlinkIdentify(message.ieee_address, message.channel);
|
|
|
|
return utils.getResponse(message, {ieee_address: message.ieee_address, channel: message.channel}, null);
|
|
|
|
}
|
|
|
|
|
2020-06-15 10:19:57 -07:00
|
|
|
async touchlinkFactoryReset(message) {
|
2020-09-08 11:24:49 -07:00
|
|
|
let result = false;
|
|
|
|
const payload = {};
|
|
|
|
if (typeof message === 'object' && message.hasOwnProperty('ieee_address') &&
|
|
|
|
message.hasOwnProperty('channel')) {
|
|
|
|
logger.info(`Start Touchlink factory reset of '${message.ieee_address}' on channel ${message.channel}`);
|
|
|
|
result = await this.zigbee.touchlinkFactoryReset(message.ieee_address, message.channel);
|
|
|
|
payload.ieee_address = message.ieee_address;
|
|
|
|
payload.channel = message.channel;
|
|
|
|
} else {
|
|
|
|
logger.info('Start Touchlink factory reset of first found device');
|
|
|
|
result = await this.zigbee.touchlinkFactoryResetFirst();
|
|
|
|
}
|
|
|
|
|
2020-06-15 10:19:57 -07:00
|
|
|
if (result) {
|
|
|
|
logger.info('Successfully factory reset device through Touchlink');
|
2020-09-08 11:24:49 -07:00
|
|
|
return utils.getResponse(message, payload, null);
|
2020-06-15 10:19:57 -07:00
|
|
|
} else {
|
|
|
|
logger.error('Failed to factory reset device through Touchlink');
|
|
|
|
throw new Error('Failed to factory reset device through Touchlink');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-08 11:24:49 -07:00
|
|
|
async touchlinkScan(message) {
|
|
|
|
logger.info('Start Touchlink scan');
|
|
|
|
const result = await this.zigbee.touchlinkScan();
|
|
|
|
const found = result.map((r) => {
|
|
|
|
return {ieee_address: r.ieeeAddr, channel: r.channel};
|
|
|
|
});
|
|
|
|
logger.info('Finished Touchlink scan');
|
|
|
|
return utils.getResponse(message, {found}, null);
|
|
|
|
}
|
|
|
|
|
2020-05-07 10:41:03 -07:00
|
|
|
/**
|
|
|
|
* Utils
|
|
|
|
*/
|
|
|
|
|
2020-06-15 09:58:32 -07:00
|
|
|
getValue(message) {
|
|
|
|
if (typeof message === 'object') {
|
|
|
|
if (!message.hasOwnProperty('value')) {
|
|
|
|
throw new Error('No value given');
|
|
|
|
}
|
|
|
|
|
|
|
|
return message.value;
|
|
|
|
} else {
|
|
|
|
return message;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-17 01:51:32 -07:00
|
|
|
async changeEntityOptions(entityType, message) {
|
2020-07-15 13:16:18 -07:00
|
|
|
if (typeof message !== 'object' || !message.hasOwnProperty('id') || !message.hasOwnProperty('options')) {
|
2020-06-13 14:28:06 -07:00
|
|
|
throw new Error(`Invalid payload`);
|
|
|
|
}
|
|
|
|
|
2020-07-15 13:16:18 -07:00
|
|
|
const ID = message.id;
|
2020-06-13 14:28:06 -07:00
|
|
|
const entity = this.getEntity(entityType, ID);
|
|
|
|
settings.changeEntityOptions(ID, message.options);
|
|
|
|
const cleanup = (o) => {
|
|
|
|
delete o.friendlyName; delete o.friendly_name; delete o.ID; delete o.type; delete o.devices;
|
|
|
|
return o;
|
2020-06-13 14:46:04 -07:00
|
|
|
};
|
2020-06-13 14:28:06 -07:00
|
|
|
const oldOptions = cleanup(entity.settings);
|
|
|
|
const newOptions = cleanup(settings.getEntity(ID));
|
2021-01-17 01:51:32 -07:00
|
|
|
await this.publishInfo();
|
2021-02-02 21:26:12 -07:00
|
|
|
|
|
|
|
logger.info(`Changed config for ${entityType} ${ID}`);
|
|
|
|
|
2020-07-15 13:16:18 -07:00
|
|
|
return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID}, null);
|
2020-06-13 14:28:06 -07:00
|
|
|
}
|
|
|
|
|
2020-12-28 14:57:35 -07:00
|
|
|
async deviceConfigureReporting(message) {
|
|
|
|
if (typeof message !== 'object' || !message.hasOwnProperty('id') || !message.hasOwnProperty('cluster') ||
|
|
|
|
!message.hasOwnProperty('maximum_report_interval') || !message.hasOwnProperty('minimum_report_interval') ||
|
|
|
|
!message.hasOwnProperty('reportable_change') || !message.hasOwnProperty('attribute')) {
|
|
|
|
throw new Error(`Invalid payload`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const ID = message.id;
|
|
|
|
const endpoint = this.getEntity('device', ID).endpoint;
|
|
|
|
await endpoint.configureReporting(message.cluster, [{
|
|
|
|
attribute: message.attribute, minimumReportInterval: message.minimum_report_interval,
|
|
|
|
maximumReportInterval: message.maximum_report_interval, reportableChange: message.reportable_change,
|
|
|
|
}]);
|
|
|
|
|
|
|
|
this.publishDevices();
|
|
|
|
|
2020-12-29 02:56:46 -07:00
|
|
|
logger.info(`Configured reporting for '${message.id}', '${message.cluster}.${message.attribute}'`);
|
|
|
|
|
2020-12-28 14:57:35 -07:00
|
|
|
return utils.getResponse(message, {
|
|
|
|
id: message.id, cluster: message.cluster, maximum_report_interval: message.maximum_report_interval,
|
|
|
|
minimum_report_interval: message.minimum_report_interval, reportable_change: message.reportable_change,
|
|
|
|
attribute: message.attribute,
|
|
|
|
}, null);
|
|
|
|
}
|
|
|
|
|
2020-06-13 10:35:09 -07:00
|
|
|
renameEntity(entityType, message) {
|
|
|
|
const deviceAndHasLast = entityType === 'device' && typeof message === 'object' && message.last === true;
|
2020-06-13 14:46:04 -07:00
|
|
|
if (typeof message !== 'object' || (!message.hasOwnProperty('from') && !deviceAndHasLast) ||
|
|
|
|
!message.hasOwnProperty('to')) {
|
2020-06-13 10:35:09 -07:00
|
|
|
throw new Error(`Invalid payload`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (deviceAndHasLast && !this.lastJoinedDeviceIeeeAddr) {
|
2020-06-13 14:46:04 -07:00
|
|
|
throw new Error('No device has joined since start');
|
2020-06-13 10:35:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const from = deviceAndHasLast ? this.lastJoinedDeviceIeeeAddr : message.from;
|
|
|
|
const to = message.to;
|
2020-09-04 12:08:20 -07:00
|
|
|
const homeAssisantRename = message.hasOwnProperty('homeassistant_rename') ?
|
|
|
|
message.homeassistant_rename : false;
|
2020-06-13 14:28:06 -07:00
|
|
|
const entity = this.getEntity(entityType, from);
|
2020-06-13 10:35:09 -07:00
|
|
|
|
|
|
|
settings.changeFriendlyName(from, to);
|
|
|
|
|
2020-09-07 08:29:53 -07:00
|
|
|
// Clear retained messages
|
|
|
|
this.mqtt.publish(entity.name, '', {retain: true});
|
|
|
|
|
2020-12-28 12:19:00 -07:00
|
|
|
const oldFriendlyName = entity.settings.friendlyName;
|
2020-06-13 10:35:09 -07:00
|
|
|
if (entity.type === 'device') {
|
|
|
|
this.publishDevices();
|
2020-12-28 12:19:00 -07:00
|
|
|
this.eventBus.emit(`deviceRenamed`, {device: entity.device, homeAssisantRename, from: oldFriendlyName, to});
|
2020-06-13 10:35:09 -07:00
|
|
|
} else {
|
|
|
|
this.publishGroups();
|
2020-12-28 12:19:00 -07:00
|
|
|
this.eventBus.emit(`groupRenamed`, {group: entity.group, homeAssisantRename, from: oldFriendlyName, to});
|
2020-06-13 10:35:09 -07:00
|
|
|
}
|
|
|
|
|
2020-10-09 13:59:10 -07:00
|
|
|
// Repulish entity state
|
|
|
|
this.publishEntityState(to, {});
|
|
|
|
|
2020-09-04 12:08:20 -07:00
|
|
|
return utils.getResponse(
|
|
|
|
message,
|
2020-12-28 12:19:00 -07:00
|
|
|
{from: oldFriendlyName, to, homeassistant_rename: homeAssisantRename},
|
2020-09-04 12:08:20 -07:00
|
|
|
null,
|
|
|
|
);
|
2020-06-13 10:35:09 -07:00
|
|
|
}
|
|
|
|
|
2020-06-13 08:22:00 -07:00
|
|
|
async removeEntity(entityType, message) {
|
2020-07-15 13:16:18 -07:00
|
|
|
const ID = typeof message === 'object' ? message.id : message.trim();
|
2020-06-13 14:28:06 -07:00
|
|
|
const entity = this.getEntity(entityType, ID);
|
2020-06-13 08:22:00 -07:00
|
|
|
|
2020-07-15 14:22:32 -07:00
|
|
|
let block = false;
|
2020-06-13 08:22:00 -07:00
|
|
|
let force = false;
|
2020-07-15 14:22:32 -07:00
|
|
|
let blockForceLog = '';
|
2020-06-13 08:22:00 -07:00
|
|
|
|
|
|
|
if (entityType === 'device' && typeof message === 'object') {
|
2020-07-15 14:22:32 -07:00
|
|
|
block = !!message.block;
|
2020-06-13 08:22:00 -07:00
|
|
|
force = !!message.force;
|
2020-07-15 14:22:32 -07:00
|
|
|
blockForceLog = ` (block: ${block}, force: ${force})`;
|
2020-07-07 12:16:35 -07:00
|
|
|
} else if (entityType === 'group' && typeof message === 'object') {
|
|
|
|
force = !!message.force;
|
2020-07-15 14:22:32 -07:00
|
|
|
blockForceLog = ` (force: ${force})`;
|
2020-06-13 08:22:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2020-07-15 14:22:32 -07:00
|
|
|
logger.info(`Removing ${entity.type} '${entity.settings.friendlyName}'${blockForceLog}`);
|
2020-06-13 08:22:00 -07:00
|
|
|
if (entity.type === 'device') {
|
2020-07-15 14:22:32 -07:00
|
|
|
if (block) {
|
|
|
|
settings.blockDevice(entity.settings.ID);
|
2020-06-13 08:22:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (force) {
|
2020-06-13 14:46:04 -07:00
|
|
|
await entity.device.removeFromDatabase();
|
2020-06-13 08:22:00 -07:00
|
|
|
} else {
|
|
|
|
await entity.device.removeFromNetwork();
|
|
|
|
}
|
|
|
|
} else {
|
2020-07-07 12:16:35 -07:00
|
|
|
if (force) {
|
|
|
|
await entity.group.removeFromDatabase();
|
|
|
|
} else {
|
|
|
|
await entity.group.removeFromNetwork();
|
|
|
|
}
|
2020-06-13 08:22:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fire event
|
|
|
|
if (entity.type === 'device') {
|
2020-09-25 07:50:12 -07:00
|
|
|
this.eventBus.emit('deviceRemoved', {resolvedEntity: entity});
|
2020-06-13 08:22:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove from configuration.yaml
|
|
|
|
if (entity.type === 'device') {
|
|
|
|
settings.removeDevice(entity.settings.ID);
|
|
|
|
} else {
|
|
|
|
settings.removeGroup(entity.settings.ID);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove from state
|
|
|
|
this.state.remove(entity.settings.ID);
|
|
|
|
|
2020-09-07 08:29:53 -07:00
|
|
|
// Clear any retained messages
|
|
|
|
this.mqtt.publish(entity.name, '', {retain: true});
|
|
|
|
|
2020-07-15 14:22:32 -07:00
|
|
|
logger.info(`Successfully removed ${entity.type} '${entity.settings.friendlyName}'${blockForceLog}`);
|
2020-06-13 08:22:00 -07:00
|
|
|
|
|
|
|
if (entity.type === 'device') {
|
2020-10-06 10:28:32 -07:00
|
|
|
this.publishGroups();
|
2020-06-13 08:22:00 -07:00
|
|
|
this.publishDevices();
|
2020-07-15 14:22:32 -07:00
|
|
|
return utils.getResponse(message, {id: ID, block, force}, null);
|
2020-06-13 08:22:00 -07:00
|
|
|
} else {
|
|
|
|
this.publishGroups();
|
2020-07-15 13:16:18 -07:00
|
|
|
return utils.getResponse(message, {id: ID, force: force}, null);
|
2020-06-13 08:22:00 -07:00
|
|
|
}
|
|
|
|
} catch (error) {
|
2020-06-13 14:46:04 -07:00
|
|
|
throw new Error(
|
2020-07-15 14:22:32 -07:00
|
|
|
`Failed to remove ${entity.type} '${entity.settings.friendlyName}'${blockForceLog} (${error})`,
|
2020-06-13 14:46:04 -07:00
|
|
|
);
|
2020-06-13 08:22:00 -07:00
|
|
|
}
|
|
|
|
}
|
2020-05-07 10:41:03 -07:00
|
|
|
|
2020-06-13 14:28:06 -07:00
|
|
|
getEntity(type, ID) {
|
|
|
|
const entity = this.zigbee.resolveEntity(ID);
|
|
|
|
if (!entity || entity.type !== type) {
|
|
|
|
throw new Error(`${utils.capitalize(type)} '${ID}' does not exist`);
|
|
|
|
}
|
|
|
|
return entity;
|
|
|
|
}
|
|
|
|
|
2020-05-04 11:06:50 -07:00
|
|
|
async publishInfo() {
|
2020-09-04 09:42:24 -07:00
|
|
|
const config = objectAssignDeep.noMutate({}, settings.get());
|
|
|
|
delete config.advanced.network_key;
|
2020-10-04 05:42:38 -07:00
|
|
|
delete config.mqtt.password;
|
2020-11-25 08:28:02 -07:00
|
|
|
config.frontend && delete config.frontend.auth_token;
|
2020-05-04 11:06:50 -07:00
|
|
|
const payload = {
|
2020-05-24 09:16:39 -07:00
|
|
|
version: this.zigbee2mqttVersion.version,
|
|
|
|
commit: this.zigbee2mqttVersion.commitHash,
|
|
|
|
coordinator: this.coordinatorVersion,
|
2020-07-30 13:31:52 -07:00
|
|
|
network: utils.toSnakeCase(await this.zigbee.getNetworkParameters()),
|
2020-07-13 14:00:33 -07:00
|
|
|
log_level: logger.getLevel(),
|
|
|
|
permit_join: await this.zigbee.getPermitJoin(),
|
2021-02-06 08:32:20 -07:00
|
|
|
restart_required: this.restartRequired,
|
2020-09-04 09:42:24 -07:00
|
|
|
config,
|
2021-01-16 04:32:15 -07:00
|
|
|
config_schema: settings.schema,
|
2020-05-04 11:06:50 -07:00
|
|
|
};
|
|
|
|
|
2021-02-06 07:44:10 -07:00
|
|
|
await this.mqtt.publish(
|
|
|
|
'bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
2020-05-04 11:06:50 -07:00
|
|
|
}
|
|
|
|
|
2020-07-13 12:45:44 -07:00
|
|
|
async publishDevices() {
|
2020-08-31 09:48:04 -07:00
|
|
|
const devices = this.zigbee.getDevices().map((device) => {
|
2020-05-04 11:06:50 -07:00
|
|
|
const definition = zigbeeHerdsmanConverters.findByDevice(device);
|
|
|
|
const resolved = this.zigbee.resolveEntity(device);
|
2020-08-31 09:48:04 -07:00
|
|
|
const endpoints = {};
|
|
|
|
for (const endpoint of device.endpoints) {
|
|
|
|
const data = {
|
|
|
|
bindings: [],
|
2020-12-28 15:12:40 -07:00
|
|
|
configured_reportings: [],
|
2020-08-31 09:48:04 -07:00
|
|
|
clusters: {
|
|
|
|
input: endpoint.getInputClusters().map((c) => c.name),
|
|
|
|
output: endpoint.getOutputClusters().map((c) => c.name),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
for (const bind of endpoint.binds) {
|
|
|
|
let target;
|
|
|
|
|
|
|
|
if (bind.target.constructor.name === 'Endpoint') {
|
|
|
|
target = {
|
|
|
|
type: 'endpoint', ieee_address: bind.target.getDevice().ieeeAddr, endpoint: bind.target.ID,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
target = {type: 'group', id: bind.target.groupID};
|
|
|
|
}
|
|
|
|
|
|
|
|
data.bindings.push({cluster: bind.cluster.name, target});
|
|
|
|
}
|
|
|
|
|
2020-12-28 15:12:40 -07:00
|
|
|
for (const configuredReporting of endpoint.configuredReportings) {
|
|
|
|
data.configured_reportings.push({
|
|
|
|
cluster: configuredReporting.cluster.name,
|
|
|
|
attribute: configuredReporting.attribute.name,
|
|
|
|
minimum_report_interval: configuredReporting.minimumReportInterval,
|
|
|
|
maximum_report_interval: configuredReporting.maximumReportInterval,
|
|
|
|
reportable_change: configuredReporting.reportableChange,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-31 09:48:04 -07:00
|
|
|
endpoints[endpoint.ID] = data;
|
|
|
|
}
|
|
|
|
|
2020-05-04 11:06:50 -07:00
|
|
|
return {
|
2020-07-13 14:00:33 -07:00
|
|
|
ieee_address: device.ieeeAddr,
|
2020-05-04 11:06:50 -07:00
|
|
|
type: device.type,
|
2020-07-13 14:00:33 -07:00
|
|
|
network_address: device.networkAddress,
|
2020-05-04 11:06:50 -07:00
|
|
|
supported: !!definition,
|
2020-07-13 14:00:33 -07:00
|
|
|
friendly_name: resolved.name,
|
2020-10-01 09:33:59 -07:00
|
|
|
definition: this.getDefinitionPayload(definition),
|
2020-07-13 14:00:33 -07:00
|
|
|
power_source: device.powerSource,
|
2020-07-15 13:16:18 -07:00
|
|
|
software_build_id: device.softwareBuildID,
|
2020-07-13 14:00:33 -07:00
|
|
|
date_code: device.dateCode,
|
2020-12-29 10:50:57 -07:00
|
|
|
model_id: device.modelID,
|
2020-05-04 11:06:50 -07:00
|
|
|
interviewing: device.interviewing,
|
2020-07-13 14:00:33 -07:00
|
|
|
interview_completed: device.interviewCompleted,
|
2020-08-31 09:48:04 -07:00
|
|
|
endpoints,
|
2020-05-04 11:06:50 -07:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2021-02-06 07:44:10 -07:00
|
|
|
await this.mqtt.publish(
|
|
|
|
'bridge/devices', stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
2020-05-04 11:06:50 -07:00
|
|
|
}
|
|
|
|
|
2020-07-13 12:45:44 -07:00
|
|
|
async publishGroups() {
|
2020-05-04 11:06:50 -07:00
|
|
|
const groups = this.zigbee.getGroups().map((group) => {
|
|
|
|
const resolved = this.zigbee.resolveEntity(group);
|
|
|
|
return {
|
2020-07-15 13:16:18 -07:00
|
|
|
id: group.groupID,
|
2020-11-11 10:11:18 -07:00
|
|
|
friendly_name: group.groupID === 901 ? 'default_bind_group' : resolved.name,
|
2020-05-04 11:06:50 -07:00
|
|
|
members: group.members.map((m) => {
|
|
|
|
return {
|
2020-07-13 14:00:33 -07:00
|
|
|
ieee_address: m.deviceIeeeAddress,
|
2020-05-04 11:06:50 -07:00
|
|
|
endpoint: m.ID,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2021-02-06 07:44:10 -07:00
|
|
|
await this.mqtt.publish(
|
|
|
|
'bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
2020-05-04 11:06:50 -07:00
|
|
|
}
|
2020-10-01 09:33:59 -07:00
|
|
|
|
|
|
|
getDefinitionPayload(definition) {
|
|
|
|
if (definition) {
|
|
|
|
return {
|
|
|
|
model: definition.model,
|
|
|
|
vendor: definition.vendor,
|
|
|
|
description: definition.description,
|
2020-11-04 14:33:00 -07:00
|
|
|
exposes: definition.exposes,
|
2021-01-31 04:31:14 -07:00
|
|
|
supports_ota: !!definition.ota,
|
2020-10-01 09:33:59 -07:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2020-05-04 11:06:50 -07:00
|
|
|
}
|
|
|
|
|
2020-05-24 09:16:39 -07:00
|
|
|
module.exports = Bridge;
|