Skip to content

Commit 6df4e39

Browse files
committed
Refactor!
1 parent b0d3c2f commit 6df4e39

20 files changed

+790
-606
lines changed

.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ install:
1313

1414
before_script:
1515
- npm test
16-
- npm run verify-homeassistant-mapping
1716
- npm run eslint
1817

1918
script:

lib/controller.js

+153-403
Large diffs are not rendered by default.

lib/extension/bridgeConfig.js

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
const settings = require('../util/settings');
2+
const logger = require('../util/logger');
3+
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
4+
5+
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
6+
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
7+
8+
class BridgeConfig {
9+
constructor(zigbee, mqtt, state, publishDeviceState) {
10+
this.zigbee = zigbee;
11+
this.mqtt = mqtt;
12+
this.state = state;
13+
this.publishDeviceState = publishDeviceState;
14+
15+
// Bind functions
16+
this.permitJoin = this.permitJoin.bind(this);
17+
this.logLevel = this.logLevel.bind(this);
18+
this.devices = this.devices.bind(this);
19+
this.rename = this.rename.bind(this);
20+
this.remove = this.remove.bind(this);
21+
22+
// Set supported options
23+
this.supportedOptions = {
24+
'permit_join': this.permitJoin,
25+
'log_level': this.logLevel,
26+
'devices': this.devices,
27+
'rename': this.rename,
28+
'remove': this.remove,
29+
};
30+
}
31+
32+
permitJoin(topic, message) {
33+
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true');
34+
}
35+
36+
logLevel(topic, message) {
37+
const level = message.toString().toLowerCase();
38+
if (allowedLogLevels.includes(level)) {
39+
logger.info(`Switching log level to '${level}'`);
40+
logger.transports.console.level = level;
41+
logger.transports.file.level = level;
42+
} else {
43+
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`);
44+
}
45+
}
46+
47+
devices(topic, message) {
48+
const devices = this.zigbee.getAllClients().map((device) => {
49+
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
50+
const friendlyDevice = settings.getDevice(device.ieeeAddr);
51+
52+
return {
53+
ieeeAddr: device.ieeeAddr,
54+
type: device.type,
55+
model: mappedDevice ? mappedDevice.model : device.modelId,
56+
friendly_name: friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr,
57+
};
58+
});
59+
60+
this.mqtt.log('devices', devices);
61+
}
62+
63+
rename(topic, message) {
64+
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` +
65+
`got ${message.toString()}`;
66+
67+
let json = null;
68+
try {
69+
json = JSON.parse(message.toString());
70+
} catch (e) {
71+
logger.error(invalid);
72+
return;
73+
}
74+
75+
// Validate message
76+
if (!json.new || !json.old) {
77+
logger.error(invalid);
78+
return;
79+
}
80+
81+
if (settings.changeFriendlyName(json.old, json.new)) {
82+
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
83+
} else {
84+
logger.error(`Failed to renamed - ${json.old} to ${json.new}`);
85+
return;
86+
}
87+
}
88+
89+
remove(topic, message) {
90+
message = message.toString();
91+
const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message);
92+
const deviceID = IDByFriendlyName ? IDByFriendlyName : message;
93+
const device = this.zigbee.getDevice(deviceID);
94+
95+
const cleanup = () => {
96+
// Remove from configuration.yaml
97+
settings.removeDevice(deviceID);
98+
99+
// Remove from state
100+
this.state.remove(deviceID);
101+
102+
logger.info(`Successfully removed ${deviceID}`);
103+
this.mqtt.log('device_removed', message);
104+
};
105+
106+
// Remove from zigbee network.
107+
if (device) {
108+
this.zigbee.removeDevice(deviceID, (error) => {
109+
if (!error) {
110+
cleanup();
111+
} else {
112+
logger.error(`Failed to remove ${deviceID}`);
113+
}
114+
});
115+
} else {
116+
cleanup();
117+
}
118+
}
119+
120+
onMQTTConnected() {
121+
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`);
122+
}
123+
124+
onMQTTMessage(topic, message) {
125+
if (!topic.match(configRegex)) {
126+
return false;
127+
}
128+
129+
const option = topic.split('/').slice(-1)[0];
130+
131+
if (!this.supportedOptions.hasOwnProperty(option)) {
132+
return false;
133+
}
134+
135+
this.supportedOptions[option](topic, message);
136+
137+
return true;
138+
}
139+
}
140+
141+
module.exports = BridgeConfig;

lib/extension/deviceConfigure.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const settings = require('../util/settings');
2+
const logger = require('../util/logger');
3+
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
4+
5+
/**
6+
* This extensions handles configuration of devices.
7+
*/
8+
class DeviceConfigure {
9+
constructor(zigbee, mqtt, state, publishDeviceState) {
10+
this.zigbee = zigbee;
11+
this.configured = [];
12+
}
13+
14+
onZigbeeStarted() {
15+
this.zigbee.getAllClients().forEach((device) => {
16+
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
17+
18+
if (mappedDevice) {
19+
this.configure(device, mappedDevice);
20+
}
21+
});
22+
}
23+
24+
onZigbeeMessage(message, device, mappedDevice) {
25+
if (device && mappedDevice) {
26+
this.configure(device, mappedDevice);
27+
}
28+
}
29+
30+
configure(device, mappedDevice) {
31+
const ieeeAddr = device.ieeeAddr;
32+
33+
if (!this.configured.includes(ieeeAddr) && mappedDevice.configure) {
34+
const friendlyName = settings.getDevice(ieeeAddr) ? settings.getDevice(ieeeAddr).friendly_name : 'unknown';
35+
36+
// Call configure function of this device.
37+
mappedDevice.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => {
38+
if (ok) {
39+
logger.info(`Succesfully configured ${friendlyName} ${ieeeAddr}`);
40+
} else {
41+
logger.error(`Failed to configure ${friendlyName} ${ieeeAddr}`);
42+
}
43+
});
44+
45+
// Setup an OnAfIncomingMsg handler if needed.
46+
if (mappedDevice.onAfIncomingMsg) {
47+
mappedDevice.onAfIncomingMsg.forEach((ep) => this.zigbee.registerOnAfIncomingMsg(ieeeAddr, ep));
48+
}
49+
50+
// Mark as configured
51+
this.configured.push(ieeeAddr);
52+
}
53+
}
54+
}
55+
56+
module.exports = DeviceConfigure;

lib/extension/devicePublish.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ const settings = require('../util/settings');
33
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
44
const Queue = require('queue');
55
const logger = require('../util/logger');
6+
67
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
78
const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right', 'top_left', 'top_right'];
9+
const maxDepth = 20;
810

911
class DevicePublish {
10-
constructor(zigbee, mqtt, state, mqttPublishDeviceState) {
12+
constructor(zigbee, mqtt, state, publishDeviceState) {
1113
this.zigbee = zigbee;
1214
this.mqtt = mqtt;
1315
this.state = state;
14-
15-
// TODO -> remove this; move to publish device state method to mqtt.js
16-
this.mqttPublishDeviceState = mqttPublishDeviceState;
16+
this.publishDeviceState = publishDeviceState;
1717

1818
/**
1919
* Setup command queue.
@@ -23,9 +23,10 @@ class DevicePublish {
2323
this.queue = new Queue();
2424
this.queue.concurrency = 1;
2525
this.queue.autostart = true;
26+
}
2627

28+
onMQTTConnected() {
2729
// Subscribe to topics.
28-
const maxDepth = 20;
2930
for (let step = 1; step < maxDepth; step++) {
3031
const topic = `${settings.get().mqtt.base_topic}/${'+/'.repeat(step)}`;
3132
this.mqtt.subscribe(`${topic}set`);
@@ -65,15 +66,15 @@ class DevicePublish {
6566
return {type: type, deviceID: deviceID, postfix: postfix};
6667
}
6768

68-
handleMQTTMessage(topic, message) {
69+
onMQTTMessage(topic, message) {
6970
topic = this.parseTopic(topic);
7071

7172
if (!topic) {
7273
return false;
7374
}
7475

7576
// Map friendlyName to ieeeAddr if possible.
76-
const ieeeAddr = settings.getIeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;
77+
const ieeeAddr = settings.getIeeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;
7778

7879
// Get device
7980
const device = this.zigbee.getDevice(ieeeAddr);
@@ -133,7 +134,7 @@ class DevicePublish {
133134
const msg = {};
134135
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
135136
msg[_key] = key === 'brightness' ? 'ON' : json['state'];
136-
this.mqttPublishDeviceState(device, msg, true);
137+
this.publishDeviceState(device, msg, true);
137138
}
138139

139140
queueCallback();

lib/extension/deviceReceive.js

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
const settings = require('../util/settings');
2+
const logger = require('../util/logger');
3+
4+
const dontCacheProperties = ['click', 'action', 'button', 'button_left', 'button_right'];
5+
6+
/**
7+
* This extensions handles messages received from devices.
8+
*/
9+
class DeviceReceive {
10+
constructor(zigbee, mqtt, state, publishDeviceState) {
11+
this.zigbee = zigbee;
12+
this.mqtt = mqtt;
13+
this.state = state;
14+
this.publishDeviceState = publishDeviceState;
15+
}
16+
17+
onZigbeeMessage(message, device, mappedDevice) {
18+
if (message.type == 'devInterview' && !settings.getDevice(message.data)) {
19+
logger.info('Connecting with device...');
20+
this.mqtt.log('pairing', 'connecting with device');
21+
}
22+
23+
if (message.type == 'devIncoming') {
24+
logger.info('Device incoming...');
25+
this.mqtt.log('pairing', 'device incoming');
26+
}
27+
28+
if (!device) {
29+
logger.warn('Message without device!');
30+
return;
31+
}
32+
33+
// Check if this is a new device.
34+
if (!settings.getDevice(device.ieeeAddr)) {
35+
logger.info(`New device with address ${device.ieeeAddr} connected!`);
36+
settings.addDevice(device.ieeeAddr);
37+
this.mqtt.log('device_connected', device.ieeeAddr);
38+
}
39+
40+
if (!mappedDevice) {
41+
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
42+
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
43+
return;
44+
}
45+
46+
// After this point we cant handle message withoud cid or cmdId anymore.
47+
if (!message.data || (!message.data.cid && !message.data.cmdId)) {
48+
return;
49+
}
50+
51+
// Find a conveter for this message.
52+
const cid = message.data.cid;
53+
const cmdId = message.data.cmdId;
54+
const converters = mappedDevice.fromZigbee.filter((c) => {
55+
if (cid) {
56+
return c.cid === cid && c.type === message.type;
57+
} else if (cmdId) {
58+
return c.cmd === cmdId;
59+
}
60+
61+
return false;
62+
});
63+
64+
// Check if there is an available converter
65+
if (!converters.length) {
66+
if (cid) {
67+
logger.warn(
68+
`No converter available for '${mappedDevice.model}' with cid '${cid}', ` +
69+
`type '${message.type}' and data '${JSON.stringify(message.data)}'`
70+
);
71+
} else if (cmdId) {
72+
logger.warn(
73+
`No converter available for '${mappedDevice.model}' with cmd '${cmdId}' ` +
74+
`and data '${JSON.stringify(message.data)}'`
75+
);
76+
}
77+
78+
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices.`);
79+
return;
80+
}
81+
82+
// Convert this Zigbee message to a MQTT message.
83+
// Get payload for the message.
84+
// - If a payload is returned publish it to the MQTT broker
85+
// - If NO payload is returned do nothing. This is for non-standard behaviour
86+
// for e.g. click switches where we need to count number of clicks and detect long presses.
87+
converters.forEach((converter) => {
88+
const publish = (payload) => {
89+
// Don't cache messages with following properties:
90+
let cache = true;
91+
dontCacheProperties.forEach((property) => {
92+
if (payload.hasOwnProperty(property)) {
93+
cache = false;
94+
}
95+
});
96+
97+
// Add device linkquality.
98+
if (message.hasOwnProperty('linkquality')) {
99+
payload.linkquality = message.linkquality;
100+
}
101+
102+
this.publishDeviceState(device, payload, cache);
103+
};
104+
105+
const payload = converter.convert(mappedDevice, message, publish, settings.getDevice(device.ieeeAddr));
106+
107+
if (payload) {
108+
publish(payload);
109+
}
110+
});
111+
}
112+
}
113+
114+
module.exports = DeviceReceive;

0 commit comments

Comments
 (0)