Skip to content

Commit

Permalink
Refactor!
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenkk committed Dec 6, 2018
1 parent b0d3c2f commit 6df4e39
Show file tree
Hide file tree
Showing 20 changed files with 790 additions and 606 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Expand Up @@ -13,7 +13,6 @@ install:

before_script:
- npm test
- npm run verify-homeassistant-mapping
- npm run eslint

script:
Expand Down
556 changes: 153 additions & 403 deletions lib/controller.js

Large diffs are not rendered by default.

141 changes: 141 additions & 0 deletions lib/extension/bridgeConfig.js
@@ -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 changes: 56 additions & 0 deletions lib/extension/deviceConfigure.js
@@ -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;
17 changes: 9 additions & 8 deletions lib/extension/devicePublish.js
Expand Up @@ -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.
Expand All @@ -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`);
Expand Down Expand Up @@ -65,15 +66,15 @@ class DevicePublish {
return {type: type, deviceID: deviceID, postfix: postfix};
}

handleMQTTMessage(topic, message) {
onMQTTMessage(topic, message) {
topic = this.parseTopic(topic);

if (!topic) {
return false;
}

// 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);
Expand Down Expand Up @@ -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();
Expand Down
114 changes: 114 additions & 0 deletions lib/extension/deviceReceive.js
@@ -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;

0 comments on commit 6df4e39

Please sign in to comment.