Refactor!

This commit is contained in:
Koenkk
2018-12-06 21:27:36 +01:00
committed by Koen Kanters
parent b0d3c2f964
commit 6df4e39f18
20 changed files with 808 additions and 624 deletions
-1
View File
@@ -13,7 +13,6 @@ install:
before_script:
- npm test
- npm run verify-homeassistant-mapping
- npm run eslint
script:
+170 -420
View File
@@ -3,474 +3,147 @@ const Zigbee = require('./zigbee');
const State = require('./state');
const logger = require('./util/logger');
const settings = require('./util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const objectAssignDeep = require('object-assign-deep');
// Extensions
const ExtensionNetworkMap = require('./extension/networkMap');
const ExtensionSoftReset = require('./extension/softReset');
const ExtensionRouterPollXiaomi = require('./extension/routerPollXiaomi');
const ExtensionDevicePublish = require('./extension/devicePublish');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const homeassistant = require('./homeassistant');
const objectAssignDeep = require('object-assign-deep');
const mqttConfigRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
/**
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
* https://community.home-assistant.io/t/missing-value-with-mqtt-only-last-data-set-is-shown/47070/9
*
* Therefore zigbee2mqtt BY DEFAULT caches all values and resend it with every message.
* advanced.cache_state in configuration.yaml allows to configure this.
* https://github.com/Koenkk/zigbee2mqtt/wiki/Configuration
*/
const cacheState = settings.get().advanced && settings.get().advanced.cache_state === false ? false : true;
if (settings.get().homeassistant && !cacheState) {
logger.warn('In order for Home Assistant integration to work properly set `cache_state: true');
}
const ExtensionHomeAssistant = require('./extension/homeassistant');
const ExtensionDeviceConfigure = require('./extension/deviceConfigure');
const ExtensionDeviceReceive = require('./extension/deviceReceive');
const ExtensionMarkOnlineXiaomi = require('./extension/markOnlineXiaomi');
const ExtensionBridgeConfig = require('./extension/bridgeConfig');
class Controller {
constructor() {
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
this.zigbee = new Zigbee(this.handleZigbeeMessage);
this.zigbee = new Zigbee();
this.mqtt = new MQTT();
this.state = new State();
this.configured = [];
this.extensions = [];
}
start() {
this.startupLogVersion(() => {
this.zigbee.start((error) => {
if (error) {
logger.error('Failed to start', error);
} else {
// Log zigbee clients on startup and configure.
const devices = this.zigbee.getAllClients();
logger.info(`Currently ${devices.length} devices are joined:`);
devices.forEach((device) => {
logger.info(this.getDeviceStartupLogMessage(device));
this.configureDevice(device);
});
// Enable zigbee join.
if (settings.get().permit_join) {
logger.warn('`permit_join` set to `true` in configuration.yaml.');
logger.warn('Allowing new devices to join.');
logger.warn('Set `permit_join` to `false` once you joined all devices.');
}
this.zigbee.permitJoin(settings.get().permit_join);
// Connect to MQTT broker
const subscriptions = [
`${settings.get().mqtt.base_topic}/bridge/config/+`,
];
if (settings.get().homeassistant) {
subscriptions.push('hass/status');
}
this.mqtt.connect(this.handleMQTTMessage, subscriptions, () => this.handleMQTTConnected());
}
});
});
}
handleMQTTConnected() {
// Home Assistant MQTT discovery on MQTT connected.
if (settings.get().homeassistant) {
// MQTT discovery of all paired devices on startup.
this.zigbee.getAllClients().forEach((device) => {
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt, true);
}
});
}
// Bind methods
this.onMQTTConnected = this.onMQTTConnected.bind(this);
this.onZigbeeMessage = this.onZigbeeMessage.bind(this);
this.onMQTTMessage = this.onMQTTMessage.bind(this);
// Initialize extensions.
this.extensions = [
new ExtensionDevicePublish(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
new ExtensionSoftReset(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
new ExtensionRouterPollXiaomi(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishDeviceState),
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishDeviceState),
new ExtensionDevicePublish(this.zigbee, this.mqtt, this.state, this.publishDeviceState),
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishDeviceState),
new ExtensionRouterPollXiaomi(this.zigbee, this.mqtt, this.state, this.publishDeviceState),
new ExtensionMarkOnlineXiaomi(this.zigbee, this.mqtt, this.state, this.publishDeviceState),
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishDeviceState),
];
if (settings.get().homeassistant) {
this.extensions.push(new ExtensionHomeAssistant(
this.zigbee, this.mqtt, this.state, this.publishDeviceState
));
}
if (settings.get().advanced.soft_reset_timeout !== 0) {
this.extensions.push(new ExtensionSoftReset(
this.zigbee, this.mqtt, this.state, this.publishDeviceState
));
}
}
onMQTTConnected() {
// Resend all cached states.
this.sendAllCachedStates();
// Call extensions
this.extensions.filter((e) => e.onMQTTConnected).forEach((e) => e.onMQTTConnected());
}
sendAllCachedStates() {
this.zigbee.getAllClients().forEach((device) => {
if (this.state.exists(device.ieeeAddr)) {
this.mqttPublishDeviceState(device, this.state.get(device.ieeeAddr), false);
}
onZigbeeStarted() {
// Log zigbee clients on startup and configure.
const devices = this.zigbee.getAllClients();
logger.info(`Currently ${devices.length} devices are joined:`);
devices.forEach((device) => {
logger.info(this.getDeviceStartupLogMessage(device));
});
}
stop(callback) {
this.extensions.filter((e) => e.stop).forEach((e) => e.stop());
this.state.save();
this.mqtt.disconnect();
this.zigbee.stop(callback);
}
configureDevice(device) {
let friendlyName = 'unknown';
const ieeeAddr = device.ieeeAddr;
if (settings.getDevice(ieeeAddr)) {
friendlyName = settings.getDevice(ieeeAddr).friendly_name;
}
if (ieeeAddr && device.modelId && !this.configured.includes(ieeeAddr)) {
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
// Call configure function of device.
if (mappedModel && mappedModel.configure) {
mappedModel.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => {
if (ok) {
logger.info(`Succesfully configured ${friendlyName} ${ieeeAddr}`);
} else {
logger.error(`Failed to configure ${friendlyName} ${ieeeAddr}`);
}
});
}
// Setup an OnAfIncomingMsg handler if needed.
if (mappedModel && mappedModel.onAfIncomingMsg) {
mappedModel.onAfIncomingMsg.forEach((ep) => this.zigbee.registerOnAfIncomingMsg(ieeeAddr, ep));
}
this.configured.push(ieeeAddr);
}
}
getDeviceStartupLogMessage(device) {
let friendlyName = 'unknown';
let type = 'unknown';
let friendlyDevice = {model: 'unkown', description: 'unknown'};
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
friendlyDevice = mappedModel;
// Enable zigbee join.
if (settings.get().permit_join) {
logger.warn('`permit_join` set to `true` in configuration.yaml.');
logger.warn('Allowing new devices to join.');
logger.warn('Set `permit_join` to `false` once you joined all devices.');
}
if (settings.getDevice(device.ieeeAddr)) {
friendlyName = settings.getDevice(device.ieeeAddr).friendly_name;
}
this.zigbee.permitJoin(settings.get().permit_join);
if (device.type) {
type = device.type;
}
// Connect to MQTT broker
this.mqtt.connect(this.onMQTTMessage, this.onMQTTConnected);
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ` +
`${friendlyDevice.vendor} ${friendlyDevice.description} (${type})`;
// Call extensions
this.extensions.filter((e) => e.onZigbeeStarted).forEach((e) => e.onZigbeeStarted());
}
getDeviceInfoForMqtt(device) {
const {type, ieeeAddr, nwkAddr, manufId, manufName, powerSource, modelId, status} = device;
const deviceSettings = settings.getDevice(device.ieeeAddr);
onZigbeeMessage(message) {
// Variables
let device = null;
let mappedDevice = null;
return {
ieeeAddr,
friendlyName: deviceSettings.friendly_name || '',
type,
nwkAddr,
manufId,
manufName,
powerSource,
modelId,
status,
};
}
handleZigbeeMessage(message) {
// Call extensions.
this.extensions.filter((e) => e.handleZigbeeMessage).forEach((e) => e.handleZigbeeMessage(message));
// Log the message.
let logMessage = `Received zigbee message of type '${message.type}' ` +
`with data '${JSON.stringify(message.data)}'`;
// Check if message has a device
if (message.endpoints && message.endpoints[0].device) {
const device = message.endpoints[0].device;
logMessage += ` of device '${device.modelId}' (${device.ieeeAddr})`;
}
logger.debug(logMessage);
if (message.type == 'devInterview' && !settings.getDevice(message.data)) {
logger.info('Connecting with device...');
this.mqtt.log('pairing', 'connecting with device');
device = message.endpoints[0].device;
}
if (message.type == 'devIncoming') {
logger.info('Device incoming...');
this.mqtt.log('pairing', 'device incoming');
// Retrieve modelId from message
if (device && device.modelId) {
mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
}
// We dont handle messages without endpoints.
if (!message.endpoints) {
return;
}
// Log
logger.debug(
`Received zigbee message of type '${message.type}' with data '${JSON.stringify(message.data)}'` +
(device ? ` of device '${device.modelId}' (${device.ieeeAddr})` : '')
);
const device = message.endpoints[0].device;
if (!device) {
logger.warn('Message without device!');
return;
}
// Check if this is a new device.
if (!settings.getDevice(device.ieeeAddr)) {
logger.info(`New device with address ${device.ieeeAddr} connected!`);
settings.addDevice(device.ieeeAddr);
this.mqtt.log('device_connected', device.ieeeAddr);
}
// We can't handle devices without modelId.
if (!device.modelId) {
return;
}
// Map Zigbee modelID to vendor modelID.
const modelID = message.endpoints[0].device.modelId;
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(modelID);
if (!mappedModel) {
logger.warn(`Device with modelID '${modelID}' is not supported.`);
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
return;
}
// Configure device.
this.configureDevice(device);
// Home Assistant MQTT discovery
if (settings.get().homeassistant) {
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt, false);
}
// After this point we cant handle message withoud cid anymore.
if (!message.data || (!message.data.cid && !message.data.cmdId)) {
return;
}
// Find a conveter for this message.
const cid = message.data.cid;
const cmdId = message.data.cmdId;
const converters = mappedModel.fromZigbee.filter((c) => {
if (cid) {
return c.cid === cid && c.type === message.type;
} else if (cmdId) {
return c.cmd === cmdId;
}
return false;
});
if (!converters.length) {
if (cid) {
logger.warn(
`No converter available for '${mappedModel.model}' with cid '${cid}', ` +
`type '${message.type}' and data '${JSON.stringify(message.data)}'`
);
} else if (cmdId) {
logger.warn(
`No converter available for '${mappedModel.model}' with cmd '${cmdId}' ` +
`and data '${JSON.stringify(message.data)}'`
);
}
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices.`);
return;
}
// Convert this Zigbee message to a MQTT message.
// Get payload for the message.
// - If a payload is returned publish it to the MQTT broker
// - If NO payload is returned do nothing. This is for non-standard behaviour
// for e.g. click switches where we need to count number of clicks and detect long presses.
converters.forEach((converter) => {
const publish = (payload) => {
// Don't cache messages with following properties:
const dontCacheProperties = ['click', 'action', 'button', 'button_left', 'button_right'];
let cache = true;
dontCacheProperties.forEach((property) => {
if (payload.hasOwnProperty(property)) {
cache = false;
}
});
// Add device linkquality.
if (message.hasOwnProperty('linkquality')) {
payload.linkquality = message.linkquality;
}
this.mqttPublishDeviceState(device, payload, cache);
};
const payload = converter.convert(mappedModel, message, publish, settings.getDevice(device.ieeeAddr));
if (payload) {
publish(payload);
}
});
// Call extensions.
this.extensions
.filter((e) => e.onZigbeeMessage)
.forEach((e) => e.onZigbeeMessage(message, device, mappedDevice));
}
handleMQTTMessage(topic, message) {
onMQTTMessage(topic, message) {
logger.debug(`Received MQTT message on '${topic}' with data '${message}'`);
// Find extensions that can handle MQTT messages and get results
// Call extensions
const results = this.extensions
.filter((e) => e.handleMQTTMessage)
.map((e) => e.handleMQTTMessage(topic, message));
.filter((e) => e.onMQTTMessage)
.map((e) => e.onMQTTMessage(topic, message));
if (topic.match(mqttConfigRegex)) {
this.handleMQTTMessageConfig(topic, message);
} else if (topic === 'hass/status') {
if (message.toString().toLowerCase() === 'online') {
const timer = setTimeout(() => {
this.sendAllCachedStates();
clearTimeout(timer);
}, 20000);
}
} else if (!results.includes(true)) {
if (!results.includes(true)) {
logger.warn(`Cannot handle MQTT message on '${topic}' with data '${message}'`);
}
}
handleMQTTMessageConfig(topic, message) {
const option = topic.split('/').slice(-1)[0];
if (option === 'permit_join') {
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true');
} else if (option === 'log_level') {
const level = message.toString().toLowerCase();
if (allowedLogLevels.includes(level)) {
logger.info(`Switching log level to '${level}'`);
logger.transports.console.level = level;
logger.transports.file.level = level;
} else {
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`);
}
} else if (option === 'devices') {
const devices = this.zigbee.getAllClients().map((device) => {
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
const friendlyDevice = settings.getDevice(device.ieeeAddr);
return {
ieeeAddr: device.ieeeAddr,
type: device.type,
model: mappedModel ? mappedModel.model : device.modelId,
friendly_name: friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr,
};
start() {
this.startupLogVersion(() => {
this.zigbee.start(this.onZigbeeMessage, (error) => {
if (error) {
logger.error('Failed to start', error);
} else {
this.onZigbeeStarted();
}
});
this.mqtt.log('devices', devices);
} else if (option === 'remove') {
message = message.toString();
const IDByFriendlyName = settings.getIDByFriendlyName(message);
const deviceID = IDByFriendlyName ? IDByFriendlyName : message;
const device = this.zigbee.getDevice(deviceID);
const cleanup = () => {
// Clear Home Assistant MQTT discovery message
if (settings.get().homeassistant && device) {
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
homeassistant.clear(deviceID, mappedModel.model, this.mqtt);
}
}
// Remove from configuration.yaml
settings.removeDevice(deviceID);
// Remove from state
this.state.remove(deviceID);
logger.info(`Successfully removed ${deviceID}`);
this.mqtt.log('device_removed', message);
};
// Remove from zigbee network.
if (device) {
this.zigbee.removeDevice(deviceID, (error) => {
if (!error) {
cleanup();
} else {
logger.error(`Failed to remove ${deviceID}`);
}
});
} else {
cleanup();
}
} else if (option === 'rename') {
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` +
`got ${message.toString()}`;
let json = null;
try {
json = JSON.parse(message.toString());
} catch (e) {
logger.error(invalid);
return;
}
// Validate message
if (!json.new || !json.old) {
logger.error(invalid);
return;
}
if (settings.changeFriendlyName(json.old, json.new)) {
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
} else {
logger.error(`Failed to renamed - ${json.old} to ${json.new}`);
return;
}
// Homeassistant rediscover
if (settings.get().homeassistant) {
const ID = settings.getIDByFriendlyName(json.new);
const device = this.zigbee.getDevice(ID);
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt, true);
}
}
} else {
logger.warn(`Cannot handle MQTT config option '${option}' with message '${message}'`);
}
});
}
mqttPublishDeviceState(device, payload, cache) {
const deviceID = device.ieeeAddr;
const appSettings = settings.get();
let messagePayload = {...payload};
stop(callback) {
// Call extensions
this.extensions.filter((e) => e.stop).forEach((e) => e.stop());
if (cacheState) {
// Add cached state to payload
if (this.state.exists(deviceID)) {
messagePayload = objectAssignDeep.noMutate(this.state.get(deviceID), payload);
}
// Update state cache with new state.
if (cache) {
this.state.set(deviceID, messagePayload);
}
}
const deviceSettings = settings.getDevice(deviceID);
const friendlyName = deviceSettings ? deviceSettings.friendly_name : deviceID;
const options = {
retain: deviceSettings ? deviceSettings.retain : false,
qos: deviceSettings && deviceSettings.qos ? deviceSettings.qos : 0,
};
if (appSettings.mqtt.include_device_information) {
messagePayload.device = this.getDeviceInfoForMqtt(device);
}
this.mqtt.publish(friendlyName, JSON.stringify(messagePayload), options);
// Wrap-up
this.state.save();
this.mqtt.disconnect();
this.zigbee.stop(callback);
}
startupLogVersion(callback) {
@@ -496,6 +169,83 @@ class Controller {
callback();
});
}
getDeviceStartupLogMessage(device) {
let friendlyName = 'unknown';
let type = 'unknown';
let friendlyDevice = {model: 'unkown', description: 'unknown'};
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
friendlyDevice = mappedModel;
}
if (settings.getDevice(device.ieeeAddr)) {
friendlyName = settings.getDevice(device.ieeeAddr).friendly_name;
}
if (device.type) {
type = device.type;
}
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ` +
`${friendlyDevice.vendor} ${friendlyDevice.description} (${type})`;
}
sendAllCachedStates() {
this.zigbee.getAllClients().forEach((device) => {
if (this.state.exists(device.ieeeAddr)) {
this.publishDeviceState(device, this.state.get(device.ieeeAddr), false);
}
});
}
publishDeviceState(device, payload, cache) {
const deviceID = device.ieeeAddr;
const appSettings = settings.get();
let messagePayload = {...payload};
if (appSettings.advanced.cache_state) {
// Add cached state to payload
if (this.state.exists(deviceID)) {
messagePayload = objectAssignDeep.noMutate(this.state.get(deviceID), payload);
}
// Update state cache with new state.
if (cache) {
this.state.set(deviceID, messagePayload);
}
}
const deviceSettings = settings.getDevice(deviceID);
const friendlyName = deviceSettings ? deviceSettings.friendly_name : deviceID;
const options = {
retain: deviceSettings ? deviceSettings.retain : false,
qos: deviceSettings && deviceSettings.qos ? deviceSettings.qos : 0,
};
if (appSettings.mqtt.include_device_information) {
messagePayload.device = this.getDeviceInfoForMqtt(device);
}
this.mqtt.publish(friendlyName, JSON.stringify(messagePayload), options);
}
getDeviceInfoForMqtt(device) {
const {type, ieeeAddr, nwkAddr, manufId, manufName, powerSource, modelId, status} = device;
const deviceSettings = settings.getDevice(device.ieeeAddr);
return {
ieeeAddr,
friendlyName: deviceSettings.friendly_name || '',
type,
nwkAddr,
manufId,
manufName,
powerSource,
modelId,
status,
};
}
}
module.exports = Controller;
+141
View File
@@ -0,0 +1,141 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
class BridgeConfig {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
this.publishDeviceState = publishDeviceState;
// Bind functions
this.permitJoin = this.permitJoin.bind(this);
this.logLevel = this.logLevel.bind(this);
this.devices = this.devices.bind(this);
this.rename = this.rename.bind(this);
this.remove = this.remove.bind(this);
// Set supported options
this.supportedOptions = {
'permit_join': this.permitJoin,
'log_level': this.logLevel,
'devices': this.devices,
'rename': this.rename,
'remove': this.remove,
};
}
permitJoin(topic, message) {
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true');
}
logLevel(topic, message) {
const level = message.toString().toLowerCase();
if (allowedLogLevels.includes(level)) {
logger.info(`Switching log level to '${level}'`);
logger.transports.console.level = level;
logger.transports.file.level = level;
} else {
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`);
}
}
devices(topic, message) {
const devices = this.zigbee.getAllClients().map((device) => {
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
const friendlyDevice = settings.getDevice(device.ieeeAddr);
return {
ieeeAddr: device.ieeeAddr,
type: device.type,
model: mappedDevice ? mappedDevice.model : device.modelId,
friendly_name: friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr,
};
});
this.mqtt.log('devices', devices);
}
rename(topic, message) {
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` +
`got ${message.toString()}`;
let json = null;
try {
json = JSON.parse(message.toString());
} catch (e) {
logger.error(invalid);
return;
}
// Validate message
if (!json.new || !json.old) {
logger.error(invalid);
return;
}
if (settings.changeFriendlyName(json.old, json.new)) {
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
} else {
logger.error(`Failed to renamed - ${json.old} to ${json.new}`);
return;
}
}
remove(topic, message) {
message = message.toString();
const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message);
const deviceID = IDByFriendlyName ? IDByFriendlyName : message;
const device = this.zigbee.getDevice(deviceID);
const cleanup = () => {
// Remove from configuration.yaml
settings.removeDevice(deviceID);
// Remove from state
this.state.remove(deviceID);
logger.info(`Successfully removed ${deviceID}`);
this.mqtt.log('device_removed', message);
};
// Remove from zigbee network.
if (device) {
this.zigbee.removeDevice(deviceID, (error) => {
if (!error) {
cleanup();
} else {
logger.error(`Failed to remove ${deviceID}`);
}
});
} else {
cleanup();
}
}
onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`);
}
onMQTTMessage(topic, message) {
if (!topic.match(configRegex)) {
return false;
}
const option = topic.split('/').slice(-1)[0];
if (!this.supportedOptions.hasOwnProperty(option)) {
return false;
}
this.supportedOptions[option](topic, message);
return true;
}
}
module.exports = BridgeConfig;
+56
View File
@@ -0,0 +1,56 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
/**
* This extensions handles configuration of devices.
*/
class DeviceConfigure {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.configured = [];
}
onZigbeeStarted() {
this.zigbee.getAllClients().forEach((device) => {
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedDevice) {
this.configure(device, mappedDevice);
}
});
}
onZigbeeMessage(message, device, mappedDevice) {
if (device && mappedDevice) {
this.configure(device, mappedDevice);
}
}
configure(device, mappedDevice) {
const ieeeAddr = device.ieeeAddr;
if (!this.configured.includes(ieeeAddr) && mappedDevice.configure) {
const friendlyName = settings.getDevice(ieeeAddr) ? settings.getDevice(ieeeAddr).friendly_name : 'unknown';
// Call configure function of this device.
mappedDevice.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => {
if (ok) {
logger.info(`Succesfully configured ${friendlyName} ${ieeeAddr}`);
} else {
logger.error(`Failed to configure ${friendlyName} ${ieeeAddr}`);
}
});
// Setup an OnAfIncomingMsg handler if needed.
if (mappedDevice.onAfIncomingMsg) {
mappedDevice.onAfIncomingMsg.forEach((ep) => this.zigbee.registerOnAfIncomingMsg(ieeeAddr, ep));
}
// Mark as configured
this.configured.push(ieeeAddr);
}
}
}
module.exports = DeviceConfigure;
+9 -8
View File
@@ -3,17 +3,17 @@ const settings = require('../util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const Queue = require('queue');
const logger = require('../util/logger');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right', 'top_left', 'top_right'];
const maxDepth = 20;
class DevicePublish {
constructor(zigbee, mqtt, state, mqttPublishDeviceState) {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
// TODO -> remove this; move to publish device state method to mqtt.js
this.mqttPublishDeviceState = mqttPublishDeviceState;
this.publishDeviceState = publishDeviceState;
/**
* Setup command queue.
@@ -23,9 +23,10 @@ class DevicePublish {
this.queue = new Queue();
this.queue.concurrency = 1;
this.queue.autostart = true;
}
onMQTTConnected() {
// Subscribe to topics.
const maxDepth = 20;
for (let step = 1; step < maxDepth; step++) {
const topic = `${settings.get().mqtt.base_topic}/${'+/'.repeat(step)}`;
this.mqtt.subscribe(`${topic}set`);
@@ -65,7 +66,7 @@ class DevicePublish {
return {type: type, deviceID: deviceID, postfix: postfix};
}
handleMQTTMessage(topic, message) {
onMQTTMessage(topic, message) {
topic = this.parseTopic(topic);
if (!topic) {
@@ -73,7 +74,7 @@ class DevicePublish {
}
// Map friendlyName to ieeeAddr if possible.
const ieeeAddr = settings.getIeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;
const ieeeAddr = settings.getIeeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;
// Get device
const device = this.zigbee.getDevice(ieeeAddr);
@@ -133,7 +134,7 @@ class DevicePublish {
const msg = {};
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
msg[_key] = key === 'brightness' ? 'ON' : json['state'];
this.mqttPublishDeviceState(device, msg, true);
this.publishDeviceState(device, msg, true);
}
queueCallback();
+114
View File
@@ -0,0 +1,114 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const dontCacheProperties = ['click', 'action', 'button', 'button_left', 'button_right'];
/**
* This extensions handles messages received from devices.
*/
class DeviceReceive {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
this.publishDeviceState = publishDeviceState;
}
onZigbeeMessage(message, device, mappedDevice) {
if (message.type == 'devInterview' && !settings.getDevice(message.data)) {
logger.info('Connecting with device...');
this.mqtt.log('pairing', 'connecting with device');
}
if (message.type == 'devIncoming') {
logger.info('Device incoming...');
this.mqtt.log('pairing', 'device incoming');
}
if (!device) {
logger.warn('Message without device!');
return;
}
// Check if this is a new device.
if (!settings.getDevice(device.ieeeAddr)) {
logger.info(`New device with address ${device.ieeeAddr} connected!`);
settings.addDevice(device.ieeeAddr);
this.mqtt.log('device_connected', device.ieeeAddr);
}
if (!mappedDevice) {
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
return;
}
// After this point we cant handle message withoud cid or cmdId anymore.
if (!message.data || (!message.data.cid && !message.data.cmdId)) {
return;
}
// Find a conveter for this message.
const cid = message.data.cid;
const cmdId = message.data.cmdId;
const converters = mappedDevice.fromZigbee.filter((c) => {
if (cid) {
return c.cid === cid && c.type === message.type;
} else if (cmdId) {
return c.cmd === cmdId;
}
return false;
});
// Check if there is an available converter
if (!converters.length) {
if (cid) {
logger.warn(
`No converter available for '${mappedDevice.model}' with cid '${cid}', ` +
`type '${message.type}' and data '${JSON.stringify(message.data)}'`
);
} else if (cmdId) {
logger.warn(
`No converter available for '${mappedDevice.model}' with cmd '${cmdId}' ` +
`and data '${JSON.stringify(message.data)}'`
);
}
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices.`);
return;
}
// Convert this Zigbee message to a MQTT message.
// Get payload for the message.
// - If a payload is returned publish it to the MQTT broker
// - If NO payload is returned do nothing. This is for non-standard behaviour
// for e.g. click switches where we need to count number of clicks and detect long presses.
converters.forEach((converter) => {
const publish = (payload) => {
// Don't cache messages with following properties:
let cache = true;
dontCacheProperties.forEach((property) => {
if (payload.hasOwnProperty(property)) {
cache = false;
}
});
// Add device linkquality.
if (message.hasOwnProperty('linkquality')) {
payload.linkquality = message.linkquality;
}
this.publishDeviceState(device, payload, cache);
};
const payload = converter.convert(mappedDevice, message, publish, settings.getDevice(device.ieeeAddr));
if (payload) {
publish(payload);
}
});
}
}
module.exports = DeviceReceive;
+55
View File
@@ -0,0 +1,55 @@
/**
* This extensions is for documentation purposes only.
* It describes all methods that are called by the controller.
*/
class ExtensionTemplate {
/**
* Besides intializing variables, the constructor should do nothing!
*
* @param {Zigbee} zigbee Zigbee controller
* @param {MQTT} mqtt MQTT controller
* @param {State} state State controller
* @param {Function} publishDeviceState Method to publish device state to MQTT.
*/
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
this.publishDeviceState = publishDeviceState;
}
/**
* This method is called by the controller once Zigbee has been started.
*/
onZigbeeStarted() {}
/**
* This method is called by the controller once connected to the MQTT server.
*/
onMQTTConnected() {}
/**
* Is called when a Zigbee message from a device is received.
* @param {Object?} message The received message (can be null)
* @param {Object?} device The device of the message (can be null)
* @param {Object?} mappedDevice The mapped device (can be null)
*/
onZigbeeMessage(message, device, mappedDevice) {}
/**
* Is called when a MQTT message is received
* @param {string} topic Topic on which the message was received
* @param {Object} message The received message
* @return {boolean} if the message was handled
*/
onMQTTMessage(topic, message) {
return false;
}
/**
* Is called once the extension has to stop
*/
stop() {}
}
module.exports = ExtensionTemplate;
@@ -1,4 +1,6 @@
const settings = require('./util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const settings = require('../util/settings');
const logger = require('../util/logger');
const configurations = {
// Binary sensor
@@ -175,7 +177,7 @@ const configurations = {
},
// Light
'light_brightness_colortemp_xy': {
'light_brightness_colortemp_colorxy': {
type: 'light',
object_id: 'light',
discovery_payload: {
@@ -186,7 +188,7 @@ const configurations = {
command_topic: true,
},
},
'light_brightness_xy': {
'light_brightness_colorxy': {
type: 'light',
object_id: 'light',
discovery_payload: {
@@ -269,21 +271,21 @@ const mapping = {
'LED1537R6': [configurations.light_brightness_colortemp],
'LED1650R5': [configurations.light_brightness],
'LED1536G5': [configurations.light_brightness_colortemp],
'7299760PH': [configurations.light_brightness_xy],
'7146060PH': [configurations.light_brightness_colortemp_xy],
'7299760PH': [configurations.light_brightness_colorxy],
'7146060PH': [configurations.light_brightness_colortemp_colorxy],
'F7C033': [configurations.light_brightness],
'JTYJ-GD-01LM/BW': [configurations.binary_sensor_smoke],
'PLUG EDP RE:DY': [configurations.switch, configurations.sensor_power],
'CC2530.ROUTER': [configurations.binary_sensor_router],
'AA70155': [configurations.light_brightness_colortemp],
'4058075816718': [configurations.light_brightness_colortemp_xy],
'AA69697': [configurations.light_brightness_colortemp_xy],
'4058075816718': [configurations.light_brightness_colortemp_colorxy],
'AA69697': [configurations.light_brightness_colortemp_colorxy],
'HALIGHTDIMWWE27': [configurations.light_brightness],
'AB3257001NJ': [configurations.switch],
'8718696449691': [configurations.light_brightness],
'RB 185 C': [configurations.light_brightness_colortemp_xy],
'9290012573A': [configurations.light_brightness_colortemp_xy],
'LED1624G9': [configurations.light_brightness_xy],
'RB 185 C': [configurations.light_brightness_colortemp_colorxy],
'9290012573A': [configurations.light_brightness_colortemp_colorxy],
'LED1624G9': [configurations.light_brightness_colorxy],
'73742': [configurations.light_brightness_colortemp],
'73740': [configurations.light_brightness_colortemp],
'22670': [configurations.light_brightness],
@@ -312,26 +314,26 @@ const mapping = {
'AA68199': [configurations.light_brightness_colortemp],
'QBKG11LM': [configurations.switch, configurations.sensor_power],
'QBKG12LM': [switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_power],
'K2RGBW01': [configurations.light_brightness_colortemp_xy],
'K2RGBW01': [configurations.light_brightness_colortemp_colorxy],
'9290011370': [configurations.light_brightness],
'DNCKATSW001': [configurations.switch],
'Z809A': [configurations.switch, configurations.sensor_power],
'NL08-0800': [configurations.light_brightness],
'915005106701': [configurations.light_brightness_colortemp_xy],
'915005106701': [configurations.light_brightness_colortemp_colorxy],
'AB32840': [configurations.light_brightness_colortemp],
'8718696485880': [configurations.light_brightness_colortemp_xy],
'8718696485880': [configurations.light_brightness_colortemp_colorxy],
'8718696598283': [configurations.light_brightness_colortemp],
'8718696695203': [configurations.light_brightness_colortemp],
'73693': [configurations.light_brightness_colortemp_xy],
'73693': [configurations.light_brightness_colortemp_colorxy],
'324131092621': [configurations.sensor_action],
'9290012607': [
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
configurations.sensor_illuminance,
],
'GL-C-008': [configurations.light_brightness_colortemp_xy],
'GL-C-008': [configurations.light_brightness_colortemp_colorxy],
'STSS-MULT-001': [configurations.binary_sensor_contact],
'E11-G23/E11-G33': [configurations.light_brightness],
'AC03645': [configurations.light_brightness_colortemp_xy],
'AC03645': [configurations.light_brightness_colortemp_colorxy],
'AC03641': [configurations.light_brightness],
'FB56+ZSW05HG1.2': [configurations.switch],
'72922-A': [configurations.switch],
@@ -345,103 +347,144 @@ const mapping = {
'BY 165': [configurations.light_brightness],
'ZLED-2709': [configurations.light_brightness],
'8718696548738': [configurations.light_brightness_colortemp],
'4052899926110': [configurations.light_brightness_colortemp_xy],
'4052899926110': [configurations.light_brightness_colortemp_colorxy],
'Z01-CIA19NAE26': [configurations.light_brightness],
'E11-N1EA': [configurations.light_brightness_colortemp_xy],
'E11-N1EA': [configurations.light_brightness_colortemp_colorxy],
'74283': [configurations.light_brightness],
'JTQJ-BF-01LM/BW': [configurations.binary_sensor_gas],
'50045': [configurations.light_brightness],
'AV2010/22': [configurations.binary_sensor_occupancy],
'3210-L': [configurations.switch],
'7299355PH': [configurations.light_brightness_xy],
'7299355PH': [configurations.light_brightness_colorxy],
'45857GE': [configurations.light_brightness],
'A6121': [configurations.sensor_lock],
'433714': [configurations.light_brightness],
'3261030P7': [configurations.light_brightness_colortemp],
'DJT11LM': [configurations.sensor_action],
'E1603': [configurations.switch],
'7199960PH': [configurations.light_brightness_xy],
'7199960PH': [configurations.light_brightness_colorxy],
'74696': [configurations.light_brightness],
'AB35996': [configurations.light_brightness_colortemp_xy],
'AB35996': [configurations.light_brightness_colortemp_colorxy],
'AB401130055': [configurations.light_brightness_colortemp],
'74282': [configurations.light_brightness_colortemp],
'RS 128 T': [configurations.light_brightness_colortemp],
'53170161': [configurations.light_brightness_colortemp],
'4058075036147': [configurations.light_brightness_colortemp_xy],
'4058075036147': [configurations.light_brightness_colortemp_colorxy],
'KS-SM001': [configurations.switch],
'MG-AUWS01': [switchWithPostfix('left'), switchWithPostfix('right')],
'9290002579A': [configurations.light_brightness_colortemp_xy],
'9290002579A': [configurations.light_brightness_colortemp_colorxy],
'4256251-RZHAC': [configurations.switch, configurations.sensor_power],
'STS-PRS-251': [configurations.binary_sensor_presence],
'4058075816794': [configurations.light_brightness_colortemp],
'4052899926158': [configurations.light_brightness],
'4058075036185': [configurations.light_brightness_colortemp_xy],
'50049': [configurations.light_brightness_xy],
'915005733701': [configurations.light_brightness_colortemp_xy],
'RB 285 C': [configurations.light_brightness_colortemp_xy],
'4058075036185': [configurations.light_brightness_colortemp_colorxy],
'50049': [configurations.light_brightness_colorxy],
'915005733701': [configurations.light_brightness_colortemp_colorxy],
'RB 285 C': [configurations.light_brightness_colortemp_colorxy],
'3216331P5': [configurations.light_brightness_colortemp],
};
// A map of all discoverd devices
const discovered = {};
/**
* This extensions handles integration with HomeAssistant
*/
class HomeAssistant {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
this.publishDeviceState = publishDeviceState;
function discover(deviceID, model, mqtt, force) {
// Check if already discoverd and check if there are configs.
const discover = force || !discovered[deviceID];
if (!discover || !mapping[model] || !settings.getDevice(deviceID)) {
return;
// A map of all discoverd devices
this.discovered = {};
if (!settings.get().advanced.cache_state) {
logger.warn('In order for HomeAssistant integration to work properly set `cache_state: true');
}
}
const friendlyName = settings.getDevice(deviceID).friendly_name;
onMQTTConnected() {
this.mqtt.subscribe('hass/status');
mapping[model].forEach((config) => {
const topic = `${config.type}/${deviceID}/${config.object_id}/config`;
const payload = {...config.discovery_payload};
payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
// MQTT discovery of all paired devices on startup.
this.zigbee.getAllClients().forEach((device) => {
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedDevice) {
this.discover(device.ieeeAddr, mappedDevice.model, true);
}
});
}
// Set unique names in cases this device produces multiple entities in homeassistant.
payload.name = mapping[model].length > 1 ? `${friendlyName}_${config.object_id}` : friendlyName;
// Only set unique_id when user did not set a friendly_name yet,
// see https://github.com/Koenkk/zigbee2mqtt/issues/138
if (deviceID === friendlyName) {
payload.unique_id = `${deviceID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
discover(ieeeAddr, model, force=false) {
// Check if already discoverd and check if there are configs.
const discover = force || !this.discovered[ieeeAddr];
if (!discover || !mapping[model] || !settings.getDevice(ieeeAddr)) {
return;
}
if (payload.command_topic) {
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`;
const friendlyName = settings.getDevice(ieeeAddr).friendly_name;
if (payload.command_topic_prefix) {
payload.command_topic += `${payload.command_topic_prefix}/`;
mapping[model].forEach((config) => {
const topic = `${config.type}/${ieeeAddr}/${config.object_id}/config`;
const payload = {...config.discovery_payload};
payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
// Set unique names in cases this device produces multiple entities in homeassistant.
payload.name = mapping[model].length > 1 ? `${friendlyName}_${config.object_id}` : friendlyName;
// Only set unique_id when user did not set a friendly_name yet,
// see https://github.com/Koenkk/zigbee2mqtt/issues/138
if (ieeeAddr === friendlyName) {
payload.unique_id = `${ieeeAddr}_${config.object_id}_${settings.get().mqtt.base_topic}`;
}
payload.command_topic += 'set';
}
if (payload.command_topic) {
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`;
mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null, 'homeassistant');
});
if (payload.command_topic_prefix) {
payload.command_topic += `${payload.command_topic_prefix}/`;
}
discovered[deviceID] = true;
}
payload.command_topic += 'set';
}
function clear(deviceID, model, mqtt) {
// Check if there are configs.
if (!mapping[model]) {
return;
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null, 'homeassistant');
});
this.discovered[ieeeAddr] = true;
}
mapping[model].forEach((config) => {
const topic = `${config.type}/${deviceID}/${config.object_id}/config`;
const payload = '';
mqtt.publish(topic, payload, {retain: true, qos: 0}, null, 'homeassistant');
});
onMQTTMessage(topic, message) {
if (!topic === 'hass/status') {
return false;
}
discovered[deviceID] = false;
if (message.toString().toLowerCase() === 'online') {
const timer = setTimeout(() => {
// Publish all device states.
this.zigbee.getAllClients().forEach((device) => {
if (this.state.exists(device.ieeeAddr)) {
this.publishDeviceState(device, this.state.get(device.ieeeAddr), false);
}
});
clearTimeout(timer);
}, 20000);
}
return true;
}
onZigbeeMessage(message, device, mappedDevice) {
if (device && mappedDevice) {
this.discover(device.ieeeAddr, mappedDevice.model);
}
}
// Only for homeassistant.test.js
_getMapping() {
return mapping;
}
}
module.exports = {
mapping: mapping,
discover: (deviceID, model, mqtt, force) => discover(deviceID, model, mqtt, force),
clear: (deviceID, model, mqtt) => clear(deviceID, model, mqtt),
};
module.exports = HomeAssistant;
+29
View File
@@ -0,0 +1,29 @@
const utils = require('../util/utils');
/**
* This extensions marks Xiaomi devices as online.
*/
class MarkOnlineXiaomi {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
}
onZigbeeStarted() {
// Set all Xiaomi devices to be online, so shepherd won't try
// to query info from devices (which would fail because they go to sleep).
const devices = this.zigbee.getAllClients();
devices.forEach((d) => {
if (utils.isXiaomiDevice(d)) {
const device = this.zigbee.shepherd.find(d.ieeeAddr, 1);
if (device) {
device.getDevice().update({
status: 'online',
joinTime: Math.floor(Date.now() / 1000),
});
}
}
});
}
}
module.exports = MarkOnlineXiaomi;
+6 -4
View File
@@ -1,16 +1,14 @@
const settings = require('../util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
class NetworkMap {
constructor(zigbee, mqtt, state) {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
// Subscribe to topic.
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
this.mqtt.subscribe(this.topic);
// Set supported formats
this.supportedFormats = {
@@ -19,7 +17,11 @@ class NetworkMap {
};
}
handleMQTTMessage(topic, message) {
onMQTTConnected() {
this.mqtt.subscribe(this.topic);
}
onMQTTMessage(topic, message) {
message = message.toString();
if (topic === this.topic && this.supportedFormats.hasOwnProperty(message)) {
+3 -1
View File
@@ -5,10 +5,12 @@ const interval = utils.secondsToMilliseconds(60);
* This extensions polls Xiaomi Zigbee routers to keep them awake.
*/
class RouterPollXiaomi {
constructor(zigbee, mqtt, state) {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.timer = null;
}
onZigbeeStarted() {
this.startTimer();
}
+5 -8
View File
@@ -6,17 +6,14 @@ const utils = require('../util/utils');
* This extensions soft resets the ZNP after a certain timeout.
*/
class SoftReset {
constructor(zigbee, mqtt, state) {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
this.timer = null;
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
}
if (this.timeout === 0) {
logger.debug(`Soft reset timeout disabled`);
} else {
logger.debug(`Soft reset timeout set to ${utils.millisecondsToSeconds(this.timeout)} seconds`);
}
onZigbeeStarted() {
logger.debug(`Soft reset timeout set to ${utils.millisecondsToSeconds(this.timeout)} seconds`);
this.resetTimer();
}
@@ -58,7 +55,7 @@ class SoftReset {
});
}
handleZigbeeMessage(message) {
onZigbeeMessage(message, device, mappedDevice) {
this.resetTimer();
}
}
+15 -21
View File
@@ -4,17 +4,18 @@ const settings = require('./util/settings');
class MQTT {
constructor() {
this.handleConnect = this.handleConnect.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.onConnect = this.onConnect.bind(this);
this.onMessage = this.onMessage.bind(this);
this.messageHandler = null;
}
connect(onMessage, subscriptions, callback) {
connect(messageHandler, callback) {
const mqttSettings = settings.get().mqtt;
logger.info(`Connecting to MQTT server at ${mqttSettings.server}`);
const options = {
will: {
topic: this.prefixTopic('bridge/state'),
topic: `${settings.get().mqtt.base_topic}/bridge/state`,
payload: 'offline',
retain: true,
},
@@ -39,11 +40,11 @@ class MQTT {
// Register callbacks.
this.client.on('connect', () => {
this.handleConnect();
this.onConnect();
callback();
});
this.client.on('message', this.handleMessage);
this.client.on('message', this.onMessage);
// Set timer at interval to check if connected to MQTT server.
const interval = 10 * 1000; // seconds * 1000.
@@ -53,8 +54,7 @@ class MQTT {
}
}, interval);
this.onMessage = onMessage;
this.subscriptions = subscriptions;
this.messageHandler = messageHandler;
}
disconnect() {
@@ -67,24 +67,23 @@ class MQTT {
});
}
handleConnect() {
onConnect() {
logger.info('Connected to MQTT server');
this.publish('bridge/state', 'online', {retain: true, qos: 0});
this.subscriptions.forEach((topic) => this.subscribe(topic));
}
subscribe(topic) {
this.client.subscribe(topic);
}
handleMessage(topic, message) {
if (this.onMessage) {
this.onMessage(topic, message);
onMessage(topic, message) {
if (this.messageHandler) {
this.messageHandler(topic, message);
}
}
publish(topic, payload, options, callback, baseTopic) {
topic = this.prefixTopic(topic, baseTopic);
publish(topic, payload, options, callback, base=settings.get().mqtt.base_topic) {
topic = `${base}/${topic}`;
if (!this.client || this.client.reconnecting) {
logger.error(`Not connected to MQTT server!`);
@@ -92,15 +91,10 @@ class MQTT {
return;
}
logger.info(`MQTT publish, topic: '${topic}', payload: '${payload}'`);
logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`);
this.client.publish(topic, payload, options, callback);
}
prefixTopic(topic, baseTopic) {
baseTopic = baseTopic ? baseTopic : settings.get().mqtt.base_topic;
return `${baseTopic}/${topic}`;
}
log(type, message) {
const payload = {type: type, message: message};
this.publish('bridge/log', JSON.stringify(payload), {retain: false});
+9 -9
View File
@@ -33,21 +33,21 @@ class State {
fs.writeFileSync(this.file, json, 'utf8');
}
exists(ID) {
return this.state.hasOwnProperty(ID);
exists(ieeeAddr) {
return this.state.hasOwnProperty(ieeeAddr);
}
get(ID) {
return this.state[ID];
get(ieeeAddr) {
return this.state[ieeeAddr];
}
set(ID, state) {
this.state[ID] = state;
set(ieeeAddr, state) {
this.state[ieeeAddr] = state;
}
remove(ID) {
if (this.exists(ID)) {
delete this.state[ID];
remove(ieeeAddr) {
if (this.exists(ieeeAddr)) {
delete this.state[ieeeAddr];
}
}
}
+34 -16
View File
@@ -7,10 +7,27 @@ const path = require('path');
const defaults = {
permit_join: false,
mqtt: {
include_device_information: false,
},
advanced: {
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
log_level: process.env.DEBUG ? 'debug' : 'info',
soft_reset_timeout: 0,
pan_id: 0x1a62,
channel: 11,
baudrate: 115200,
rtscts: true,
/**
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
* https://community.home-assistant.io/t/missing-value-with-mqtt-only-last-data-set-is-shown/47070/9
*
* Therefore zigbee2mqtt BY DEFAULT caches all values and resend it with every message.
* advanced.cache_state in configuration.yaml allows to configure this.
* https://github.com/Koenkk/zigbee2mqtt/wiki/Configuration
*/
cache_state: true,
},
};
@@ -29,40 +46,40 @@ function read() {
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
}
function addDevice(id) {
function addDevice(ieeeAddr) {
if (!settings.devices) {
settings.devices = {};
}
settings.devices[id] = {friendly_name: id, retain: false};
settings.devices[ieeeAddr] = {friendly_name: ieeeAddr, retain: false};
writeRead();
}
function removeDevice(id) {
if (settings.devices && settings.devices[id]) {
delete settings.devices[id];
function removeDevice(ieeeAddr) {
if (settings.devices && settings.devices[ieeeAddr]) {
delete settings.devices[ieeeAddr];
writeRead();
}
}
function getIDByFriendlyName(friendlyName) {
function getIeeeAddrByFriendlyName(friendlyName) {
if (!settings.devices) {
return null;
}
return Object.keys(settings.devices).find((id) =>
settings.devices[id].friendly_name === friendlyName
return Object.keys(settings.devices).find((ieeeAddr) =>
settings.devices[ieeeAddr].friendly_name === friendlyName
);
}
function changeFriendlyName(old, new_) {
const ID = getIDByFriendlyName(old);
const ieeeAddr = getIeeeAddrByFriendlyName(old);
if (!ID) {
if (!ieeeAddr) {
return false;
}
settings.devices[ID].friendly_name = new_;
settings.devices[ieeeAddr].friendly_name = new_;
writeRead();
return true;
}
@@ -70,10 +87,11 @@ function changeFriendlyName(old, new_) {
module.exports = {
get: () => objectAssignDeep.noMutate(defaults, settings),
write: () => write(),
getDevice: (id) => settings.devices ? settings.devices[id] : null,
addDevice: (id) => addDevice(id),
removeDevice: (id) => removeDevice(id),
getIDByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
getDevice: (ieeeAddr) => settings.devices ? settings.devices[ieeeAddr] : null,
addDevice: (ieeeAddr) => addDevice(ieeeAddr),
removeDevice: (ieeeAddr) => removeDevice(ieeeAddr),
getIeeeAddrByFriendlyName: (friendlyName) => getIeeeAddrByFriendlyName(friendlyName),
changeFriendlyName: (old, new_) => changeFriendlyName(old, new_),
getIeeAddrByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
};
+29 -47
View File
@@ -6,32 +6,31 @@ const zclPacket = require('zcl-packet');
const utils = require('./util/utils');
const advancedSettings = settings.get().advanced;
const shepherdSettings = {
net: {
panId: advancedSettings && advancedSettings.pan_id ? advancedSettings.pan_id : 0x1a62,
channelList: [advancedSettings && advancedSettings.channel ? advancedSettings.channel : 11],
panId: advancedSettings.pan_id,
channelList: [advancedSettings.channel],
},
dbPath: data.joinPath('database.db'),
sp: {
baudRate: advancedSettings && advancedSettings.baudrate ? advancedSettings.baudrate : 115200,
rtscts: advancedSettings && (typeof(advancedSettings.rtscts) === 'boolean') ? advancedSettings.rtscts : true,
baudRate: advancedSettings.baudrate,
rtscts: advancedSettings.rtscts,
},
};
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`);
class Zigbee {
constructor(onMessage) {
this.onMessage = onMessage;
this.handleReady = this.handleReady.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.handleError = this.handleError.bind(this);
constructor() {
this.onReady = this.onReady.bind(this);
this.onMessage = this.onMessage.bind(this);
this.onError = this.onError.bind(this);
this.messageHandler = null;
}
start(callback) {
start(messageHandler, callback) {
logger.info(`Starting zigbee-shepherd`);
this.messageHandler = messageHandler;
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
this.shepherd.start((error) => {
@@ -49,24 +48,24 @@ class Zigbee {
);
callback(error);
} else {
this._logStartupInfo();
this.logStartupInfo();
callback(null);
}
});
}, 60 * 1000);
}, utils.secondsToMilliseconds(60));
} else {
this._logStartupInfo();
this.logStartupInfo();
callback(null);
}
});
// Register callbacks.
this.shepherd.on('ready', this.handleReady);
this.shepherd.on('ind', this.handleMessage);
this.shepherd.on('error', this.handleError);
this.shepherd.on('ready', this.onReady);
this.shepherd.on('ind', this.onMessage);
this.shepherd.on('error', this.onError);
}
_logStartupInfo() {
logStartupInfo() {
logger.info('zigbee-shepherd started');
logger.info(`Coordinator firmware version: '${this.shepherd.info().firmware.revision}'`);
logger.debug(`zigbee-shepherd info: ${JSON.stringify(this.shepherd.info())}`);
@@ -83,22 +82,7 @@ class Zigbee {
});
}
handleReady() {
// Set all Xiaomi devices to be online, so shepherd won't try
// to query info from devices (which would fail because they go to sleep).
const devices = this.getAllClients();
devices.forEach((d) => {
if (utils.isXiaomiDevice(d)) {
const device = this.shepherd.find(d.ieeeAddr, 1);
if (device) {
device.getDevice().update({
status: 'online',
joinTime: Math.floor(Date.now() / 1000),
});
}
}
});
onReady() {
// Check if we have to turn off the led
if (settings.get().serial.disable_led) {
this.shepherd.controller.request('UTIL', 'ledControl', {ledid: 3, mode: 0});
@@ -107,7 +91,7 @@ class Zigbee {
logger.info('zigbee-shepherd ready');
}
handleError(message) {
onError(message) {
// This event may appear if zigbee-shepherd cannot decode bad packets (invalid checksum).
logger.error(message);
}
@@ -134,14 +118,14 @@ class Zigbee {
this.shepherd.remove(deviceID, (error) => {
if (error) {
logger.warn(`Failed to remove '${deviceID}', trying force remove...`);
this._forceRemove(deviceID, callback);
this.forceRemove(deviceID, callback);
} else {
callback(null);
}
});
}
_forceRemove(deviceID, callback) {
forceRemove(deviceID, callback) {
const device = this.shepherd._findDevByAddr(deviceID);
if (device) {
@@ -167,9 +151,9 @@ class Zigbee {
}
}
handleMessage(message) {
if (this.onMessage) {
this.onMessage(message);
onMessage(message) {
if (this.messageHandler) {
this.messageHandler(message);
}
}
@@ -187,7 +171,7 @@ class Zigbee {
}
publish(ieeAddr, cid, cmd, cmdType, zclData, cfg, ep, callback) {
const device = this._findDevice(ieeAddr, ep);
const device = this.findDevice(ieeAddr, ep);
if (!device) {
logger.error(`Zigbee cannot publish message to device because '${ieeAddr}' not known by zigbee-shepherd`);
return;
@@ -207,8 +191,6 @@ class Zigbee {
}
callback(error, rsp);
// TODO: Send read reponse back to MQTT.
};
if (cmdType === 'functional') {
@@ -229,7 +211,7 @@ class Zigbee {
}
registerOnAfIncomingMsg(ieeeAddr, ep) {
const device = this._findDevice(ieeeAddr, ep);
const device = this.findDevice(ieeeAddr, ep);
device.onAfIncomingMsg = (message) => {
// Parse the message
zclPacket.parse(message.data, message.clusterid, (error, zclData) => {
@@ -238,12 +220,12 @@ class Zigbee {
data: zclData,
};
this.handleMessage(message);
this.onMessage(message);
});
};
}
_findDevice(deviceID, ep) {
findDevice(deviceID, ep) {
// Find device in zigbee-shepherd
let device = this.getDevice(deviceID);
if (!device || !device.epList || !device.epList.length) {
-13
View File
@@ -1,13 +0,0 @@
// Verify that there are homeassistant mappings for every device.
const devices = require('zigbee-shepherd-converters').devices;
const homeassistant = require('../lib/homeassistant');
let failed = false;
devices.forEach((d) => {
if (!homeassistant.mapping[d.model]) {
console.error(`Missing homeassistant mapping for '${d.model}'`);
failed = true;
}
});
process.exit(failed ? 1 : 0);
-2
View File
@@ -1,5 +1,3 @@
/* global describe, it */
const chai = require('chai');
const proxyquire = require('proxyquire').noPreserveCache();
const data = () => proxyquire('../lib/util/data.js', {});
-2
View File
@@ -1,5 +1,3 @@
/* global describe, it */
const chai = require('chai');
const sinon = require('sinon');
const DevicePublish = require('../lib/extension/devicePublish');
+18
View File
@@ -0,0 +1,18 @@
const devices = require('zigbee-shepherd-converters').devices;
const HomeassistantExtension = require('../lib/extension/homeassistant');
const chai = require('chai');
const homeassistant = new HomeassistantExtension(null, null, null, null);
describe('HomeAssistant extension', () => {
it('Should have mapping for all devices supported by zigbee-shepherd-converters', () => {
const missing = [];
devices.forEach((d) => {
if (!homeassistant._getMapping()[d.model]) {
missing.push(d.model);
}
});
chai.assert.strictEqual(missing.length, 0, `Missing HomeAssistant mapping for: ${missing.join(', ')}`);
});
});