Compare commits

..
19 Commits
Author SHA1 Message Date
Koenkk b0d3c2f964 0.2.0 2018-11-14 19:18:31 +01:00
Koenkk f797a12250 Update zigbee-shepherd-converters. 2018-11-14 18:32:02 +01:00
Jonas Janz 081d3c3dfe update docs for Home Hssistant (#583)
This adds a hint that you 'need' to set "hass/state" as a topic if you want zigbee2mqtt to resend the cached values on restarts.
2018-11-11 20:04:34 +01:00
Koenkk 7ca966ce39 npm is not installed by default anymore in Alpine. 2018-11-07 20:25:38 +01:00
Koenkk fc664bdc68 Fix node version to >=8.11 10 and implement check on startup. https://github.com/Koenkk/zigbee2mqtt/issues/552 2018-11-07 20:05:14 +01:00
Koenkk 2cb27b14f9 Allow ‘/‘ in friendly_name. https://github.com/Koenkk/zigbee2mqtt/issues/470 2018-11-07 19:40:58 +01:00
Koenkk 86a102e081 Only mock state after set. #518 2018-11-06 21:20:03 +01:00
Koenkk 9119e54c2c Prepare for read commands. https://github.com/Koenkk/zigbee2mqtt/issues/518 2018-11-05 21:55:30 +01:00
Dustin Wyatt f379bc0391 add linkquality to json_attributes for HA discovery (#566) 2018-11-05 19:39:36 +01:00
Koenkk 2e736bf501 Update zigbee-shepherd-converters to 4.0.11 2018-10-31 17:10:28 +01:00
Stanislav Demydiuk 1e7f985d4e Support both DeviceID and FriendlyName in device control topic (#476) 2018-10-30 21:23:34 +01:00
Koenkk 61c4ff33e0 Update zigbee-shepherd-converters to 4.0.10 2018-10-29 21:34:48 +01:00
mmnpkf 73ae48f387 Add Sengled Element Classic (A60) - B22 fitting (#544)
* Add Sengled Element Classic (A60) - B22 fitting

The Sengled Element Classic with Edison fitting (E27) is already defined - model E11-G23.  The same bulb comes with a bayonet fitting (B22) with a slightly different model number - E11-G33.

* Update homeassistant.js

* Update homeassistant.js
2018-10-29 21:12:37 +01:00
Stanislav Demydiuk 0a1c60b2cf Add Link for Domoticz integration to Readme (#546) 2018-10-29 08:29:28 +01:00
Koenkk 4eb56319c4 Update zigbee-shepherd-converters to 4.0.9 2018-10-25 20:44:55 +02:00
mmnpkf d8ea36af29 Add Nue Double GPO (au version) to Home Assistant (#506)
* Update homeassistant.js

Add code to expose Nue double power outlet (au version) to Home Assistant

* Update homeassistant.js
2018-10-25 20:43:31 +02:00
Felix Triller d0cf61bf10 Use Home Assistant's device_class (#528)
* Use Home Assistant's device_class for sensors.

* Unit for pressure should be hPa

* There is no device_class lock
2018-10-24 21:10:42 +02:00
Koenkk ddb83cc6e4 Handle zigbee commands sequentially (and refactor it). #529 2018-10-23 20:39:48 +02:00
Jens Willmer e54de4bea7 Adding update script (#525)
* Adding update script

Fix #389

* Update update.sh :)

* Make executable & update update.sh
2018-10-22 18:08:03 +02:00
19 changed files with 906 additions and 594 deletions
+1
View File
@@ -1,5 +1,6 @@
{
"env": {
"mocha": true,
"es6": true,
"node": true
},
+2 -1
View File
@@ -62,4 +62,5 @@ data/database.db
data/config.json
data/log*.txt
data/state.json
data/log
data/log
data-backup/
+1 -1
View File
@@ -6,7 +6,7 @@ services:
- docker
node_js:
- "8"
- "10"
install:
- npm install
+5
View File
@@ -24,6 +24,11 @@ Zigbee2mqtt integrates well with (almost) every home automation solution because
- [Hassio](https://www.home-assistant.io/hassio/): Using [the official addon](https://github.com/danielwelch/hassio-zigbee2mqtt) from [danielwelch](https://github.com/danielwelch)
- Generic install or Hassbian: Using instructions [here](https://github.com/Koenkk/zigbee2mqtt/wiki/Integrating-with-Home-Assistant)
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
### [Domoticz](https://www.domoticz.com/)
- Integration implemented in [domoticz-zigbee2mqtt-plugin](https://github.com/stas-demydiuk/domoticz-zigbee2mqtt-plugin)
## Architecture
![Architecture](architecture.png)
+2 -2
View File
@@ -1,4 +1,4 @@
FROM alpine:3.7
FROM alpine:3.8
# Copy files
ADD . /app
@@ -12,7 +12,7 @@ ARG COMMIT
RUN echo "{\"hash\": \"$COMMIT\"}" > .hash.json
# Install dependencies
RUN apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
RUN apk add --update --no-cache make gcc g++ python linux-headers udev nodejs=8.11.4-r0 npm=8.11.4-r0 git && \
npm install --unsafe-perm && \
apk del make gcc g++ python linux-headers udev git
+2 -2
View File
@@ -1,4 +1,4 @@
FROM arm32v6/alpine:3.7
FROM arm32v6/alpine:3.8
# Setup cross-build
ENV QEMU_EXECVE 1
@@ -14,7 +14,7 @@ ADD . /app
RUN [ "qemu-arm-static", "/bin/sh", "-c", \
"cp /app/data/configuration.yaml /app && \
cp /app/docker/run.sh /app && chmod +x /app/run.sh && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs=8.11.4-r0 npm=8.11.4-r0 git && \
npm install --unsafe-perm && \
apk del make gcc g++ python linux-headers udev git"]
+2 -2
View File
@@ -1,4 +1,4 @@
FROM arm64v8/alpine:3.7
FROM arm64v8/alpine:3.8
# Setup cross-build
ENV QEMU_EXECVE 1
@@ -14,7 +14,7 @@ ADD . /app
RUN [ "qemu-aarch64-static", "/bin/sh", "-c", \
"cp /app/data/configuration.yaml /app && \
cp /app/docker/run.sh /app && chmod +x /app/run.sh && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs=8.11.4-r0 npm=8.11.4-r0 git && \
npm install --unsafe-perm && \
apk del make gcc g++ python linux-headers udev git"]
+8 -1
View File
@@ -1,5 +1,12 @@
const Controller = require('./lib/controller');
const semver = require('semver');
const engines = require('./package.json').engines;
const version = engines.node;
if (!semver.satisfies(process.version, version)) {
console.log(`\t\tZigbee2mqtt requires node version ${version}, you are running ${process.version}!\n`); // eslint-disable-line
}
const Controller = require('./lib/controller');
const controller = new Controller();
controller.start();
+12 -117
View File
@@ -6,14 +6,12 @@ const settings = require('./util/settings');
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 mqttDeviceRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d.-]+/set`, 'g');
const mqttDevicePrefixRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d.-]+/[\\w\\s\\d.-]+/set`, 'g');
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
@@ -67,8 +65,6 @@ class Controller {
// Connect to MQTT broker
const subscriptions = [
`${settings.get().mqtt.base_topic}/+/set`,
`${settings.get().mqtt.base_topic}/+/+/set`,
`${settings.get().mqtt.base_topic}/bridge/config/+`,
];
@@ -96,9 +92,10 @@ class Controller {
// Initialize extensions.
this.extensions = [
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state),
new ExtensionSoftReset(this.zigbee, this.mqtt, this.state),
new ExtensionRouterPollXiaomi(this.zigbee, this.mqtt, this.state),
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),
];
// Resend all cached states.
@@ -320,18 +317,15 @@ class Controller {
}
handleMQTTMessage(topic, message) {
logger.debug(`Received mqtt message on topic '${topic}' with data '${message}'`);
logger.debug(`Received MQTT message on '${topic}' with data '${message}'`);
// Find extensions that could handle this.
const extensions = this.extensions.filter((e) => e.handleMQTTMessage);
// Call extensions.
const extensionResults = extensions.map((e) => e.handleMQTTMessage(topic, message));
// Find extensions that can handle MQTT messages and get results
const results = this.extensions
.filter((e) => e.handleMQTTMessage)
.map((e) => e.handleMQTTMessage(topic, message));
if (topic.match(mqttConfigRegex)) {
this.handleMQTTMessageConfig(topic, message);
} else if (topic.match(mqttDeviceRegex) || topic.match(mqttDevicePrefixRegex)) {
this.handleMQTTMessageDevice(topic, message, topic.match(mqttDevicePrefixRegex));
} else if (topic === 'hass/status') {
if (message.toString().toLowerCase() === 'online') {
const timer = setTimeout(() => {
@@ -339,8 +333,8 @@ class Controller {
clearTimeout(timer);
}, 20000);
}
} else if (!extensionResults.includes(true)) {
logger.warn(`Cannot handle MQTT message with topic '${topic}' and message '${message}'`);
} else if (!results.includes(true)) {
logger.warn(`Cannot handle MQTT message on '${topic}' with data '${message}'`);
}
}
@@ -448,105 +442,6 @@ class Controller {
}
}
handleMQTTMessageDevice(topic, message, withPrefix) {
const friendlyName = topic.split('/').slice(withPrefix ? -3 : -2)[0];
const topicPrefix = withPrefix ? topic.split('/').slice(-2)[0] : '';
// Map friendlyName to deviceID.
const deviceID = settings.getIDByFriendlyName(friendlyName);
if (!deviceID) {
logger.error(`Cannot handle '${topic}' because deviceID of '${friendlyName}' cannot be found`);
return;
}
// Convert the MQTT message to a Zigbee message.
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
// Cannot be parsed to JSON, assume state message.
json = {state: message.toString()};
}
// Find ep for this device
const device = this.zigbee.getDevice(deviceID);
if (!device) {
logger.error(`Failed to find device with deviceID ${deviceID}`);
return;
}
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (!mappedModel) {
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;
}
const ep = mappedModel.ep && mappedModel.ep[topicPrefix] ? mappedModel.ep[topicPrefix] : null;
const published = [];
Object.keys(json).forEach((key) => {
// Find converter for this key.
const converter = mappedModel.toZigbee.find((c) => c.key === key);
if (!converter) {
logger.error(`No converter available for '${key}' (${json[key]})`);
return;
}
const message = converter.convert(json[key], json);
if (!message) {
return;
}
const callback = (error) => {
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
if (!error && (key.startsWith('state') || key === 'brightness')) {
const msg = {};
const _key = topicPrefix ? `state_${topicPrefix}` : 'state';
msg[_key] = key === 'brightness' ? 'ON' : json['state'];
this.mqttPublishDeviceState(device, msg, true);
}
};
this.zigbee.publish(deviceID, message.cid, message.cmd, message.zclData,
message.cfg, ep, message.type, callback);
published.push({message: message, converter: converter});
});
/**
* After publishing a command to a zigbee device we want to monitor the changed attribute(s) so that
* everything stays in sync.
*/
published.forEach((p) => {
let counter = 0;
let secondsToMonitor = 1;
// In case of a transition we need to monitor for the whole transition time.
if (p.message.zclData.hasOwnProperty('transtime')) {
// Note that: transtime 10 = 0.1 seconds, 100 = 1 seconds, etc.
secondsToMonitor = (p.message.zclData.transtime / 10) + 1;
}
const timer = setInterval(() => {
counter++;
// Doing a 'read' will result in the device sending a zigbee message with the current attribute value.
// which will be handled by this.handleZigbeeMessage.
p.converter.attr.forEach((attribute) => {
this.zigbee.read(deviceID, p.message.cid, attribute, ep, () => null);
});
if (counter >= secondsToMonitor) {
clearTimeout(timer);
}
}, 1000);
});
}
mqttPublishDeviceState(device, payload, cache) {
const deviceID = device.ieeeAddr;
const appSettings = settings.get();
+149
View File
@@ -0,0 +1,149 @@
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'];
class DevicePublish {
constructor(zigbee, mqtt, state, mqttPublishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
// TODO -> remove this; move to publish device state method to mqtt.js
this.mqttPublishDeviceState = mqttPublishDeviceState;
/**
* Setup command queue.
* The command queue ensures that only 1 command is executed at a time.
* When executing multiple commands at the same time, some commands may fail.
*/
this.queue = new Queue();
this.queue.concurrency = 1;
this.queue.autostart = true;
// 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`);
this.mqtt.subscribe(`${topic}get`);
}
}
stop() {
this.queue.stop();
}
parseTopic(topic) {
if (!topic.match(topicRegex)) {
return null;
}
// Remove base from topic
topic = topic.replace(`${settings.get().mqtt.base_topic}/`, '');
// Parse type from topic
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
// Remove type from topic
topic = topic.replace(`/${type}`, '');
// Check if we have to deal with a postfix.
let postfix = null;
if (postfixes.find((p) => topic.endsWith(p))) {
postfix = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
// Remove postfix from topic
topic = topic.replace(`/${postfix}`, '');
}
const deviceID = topic;
return {type: type, deviceID: deviceID, postfix: postfix};
}
handleMQTTMessage(topic, message) {
topic = this.parseTopic(topic);
if (!topic) {
return false;
}
// Map friendlyName to ieeeAddr if possible.
const ieeeAddr = settings.getIeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;
// Get device
const device = this.zigbee.getDevice(ieeeAddr);
if (!device) {
logger.error(`Failed to find device with ieeAddr: '${ieeeAddr}'`);
return;
}
// Map device to a model
const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (!model) {
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;
}
// Convert the MQTT message to a Zigbee message.
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
// Cannot be parsed to JSON, assume state message.
json = {state: message.toString()};
}
// Determine endpoint to publish to.
const endpoint = model.hasOwnProperty('ep') && model.ep.hasOwnProperty(topic.postfix) ?
model.ep[topic.postfix] : null;
// For each key in the JSON message find the matching converter.
Object.keys(json).forEach((key) => {
const converter = model.toZigbee.find((c) => c.key === key);
if (!converter) {
logger.error(`No converter available for '${key}' (${json[key]})`);
return;
}
// Converter didn't return a result, skip
const converted = converter.convert(json[key], json, topic.type);
if (!converted) {
return;
}
// Add job to queue
this.queue.push((queueCallback) => {
this.zigbee.publish(
ieeeAddr,
converted.cid,
converted.cmd,
converted.cmdType,
converted.zclData,
converted.cfg,
endpoint,
(error, rsp) => {
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
if (topic.type === 'set' && !error && (key.startsWith('state') || key === 'brightness')) {
const msg = {};
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
msg[_key] = key === 'brightness' ? 'ON' : json['state'];
this.mqttPublishDeviceState(device, msg, true);
}
queueCallback();
}
);
});
});
return true;
}
}
module.exports = DevicePublish;
+2
View File
@@ -30,6 +30,8 @@ class NetworkMap {
return true;
}
return false;
}
raw(zigbee, topology) {
+44 -22
View File
@@ -10,7 +10,18 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.occupancy }}',
device_class: 'motion',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_presence': {
type: 'binary_sensor',
object_id: 'presence',
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.presence }}',
device_class: 'presence',
json_attributes: ['battery', 'voltage', 'action'],
},
},
'binary_sensor_contact': {
@@ -21,7 +32,7 @@ const configurations = {
payload_off: true,
value_template: '{{ value_json.contact }}',
device_class: 'door',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_water_leak': {
@@ -32,7 +43,7 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.water_leak }}',
device_class: 'moisture',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_smoke': {
@@ -43,7 +54,7 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.smoke }}',
device_class: 'smoke',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_gas': {
@@ -54,7 +65,7 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.gas }}',
device_class: 'gas',
json_attributes: ['sensitivity'],
json_attributes: ['linkquality', 'sensitivity'],
},
},
'binary_sensor_router': {
@@ -65,7 +76,7 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.state }}',
device_class: 'connectivity',
json_attributes: ['description', 'type', 'rssi'],
json_attributes: ['linkquality', 'description', 'type', 'rssi'],
},
},
@@ -75,9 +86,9 @@ const configurations = {
object_id: 'illuminance',
discovery_payload: {
unit_of_measurement: 'lx',
icon: 'mdi:theme-light-dark',
device_class: 'illuminance',
value_template: '{{ value_json.illuminance }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_humidity': {
@@ -85,9 +96,9 @@ const configurations = {
object_id: 'humidity',
discovery_payload: {
unit_of_measurement: '%',
icon: 'mdi:water-percent',
device_class: 'humidity',
value_template: '{{ value_json.humidity }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_temperature': {
@@ -95,19 +106,19 @@ const configurations = {
object_id: 'temperature',
discovery_payload: {
unit_of_measurement: '°C',
icon: 'mdi:temperature-celsius',
device_class: 'temperature',
value_template: '{{ value_json.temperature }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_pressure': {
type: 'sensor',
object_id: 'pressure',
discovery_payload: {
unit_of_measurement: 'Pa',
icon: 'mdi:speedometer',
unit_of_measurement: 'hPa',
device_class: 'pressure',
value_template: '{{ value_json.pressure }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_click': {
@@ -116,7 +127,7 @@ const configurations = {
discovery_payload: {
icon: 'mdi:toggle-switch',
value_template: '{{ value_json.click }}',
json_attributes: ['battery', 'voltage', 'action', 'duration'],
json_attributes: ['linkquality', 'battery', 'voltage', 'action', 'duration'],
force_update: true,
},
},
@@ -127,7 +138,7 @@ const configurations = {
unit_of_measurement: 'Watt',
icon: 'mdi:flash',
value_template: '{{ value_json.power }}',
json_attributes: ['voltage', 'temperature', 'consumption', 'current', 'power_factor'],
json_attributes: ['linkquality', 'voltage', 'temperature', 'consumption', 'current', 'power_factor'],
},
},
'sensor_action': {
@@ -137,7 +148,7 @@ const configurations = {
icon: 'mdi:gesture-double-tap',
value_template: '{{ value_json.action }}',
json_attributes: [
'battery', 'voltage', 'angle', 'side', 'from_side', 'to_side', 'brightness',
'linkquality', 'battery', 'voltage', 'angle', 'side', 'from_side', 'to_side', 'brightness',
'angle_x_absolute', 'angle_y_absolute', 'angle_z', 'angle_y', 'angle_x', 'unknown_data',
],
force_update: true,
@@ -150,7 +161,7 @@ const configurations = {
unit_of_measurement: 'brightness',
icon: 'mdi:brightness-5',
value_template: '{{ value_json.brightness }}',
json_attributes: [],
json_attributes: ['linkquality'],
},
},
'sensor_lock': {
@@ -159,7 +170,7 @@ const configurations = {
discovery_payload: {
icon: 'mdi:lock',
value_template: '{{ value_json.inserted }}',
json_attributes: ['forgotten', 'keyerror'],
json_attributes: ['linkquality', 'forgotten', 'keyerror'],
},
},
@@ -228,7 +239,7 @@ const switchWithPostfix = (postfix) => {
value_template: `{{ value_json.state_${postfix} }}`,
command_topic: true,
command_topic_prefix: postfix,
json_attributes: [`button_${postfix}`],
json_attributes: ['linkquality', `button_${postfix}`],
},
};
};
@@ -319,7 +330,7 @@ const mapping = {
],
'GL-C-008': [configurations.light_brightness_colortemp_xy],
'STSS-MULT-001': [configurations.binary_sensor_contact],
'E11-G23': [configurations.light_brightness],
'E11-G23/E11-G33': [configurations.light_brightness],
'AC03645': [configurations.light_brightness_colortemp_xy],
'AC03641': [configurations.light_brightness],
'FB56+ZSW05HG1.2': [configurations.switch],
@@ -358,6 +369,17 @@ const mapping = {
'53170161': [configurations.light_brightness_colortemp],
'4058075036147': [configurations.light_brightness_colortemp_xy],
'KS-SM001': [configurations.switch],
'MG-AUWS01': [switchWithPostfix('left'), switchWithPostfix('right')],
'9290002579A': [configurations.light_brightness_colortemp_xy],
'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],
'3216331P5': [configurations.light_brightness_colortemp],
};
// A map of all discoverd devices
+1
View File
@@ -75,4 +75,5 @@ module.exports = {
removeDevice: (id) => removeDevice(id),
getIDByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
changeFriendlyName: (old, new_) => changeFriendlyName(old, new_),
getIeeAddrByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
};
+15 -23
View File
@@ -177,8 +177,8 @@ class Zigbee {
return this.shepherd.list();
}
getDevice(deviceID) {
return this.getDevices().find((d) => d.ieeeAddr === deviceID);
getDevice(ieeeAddr) {
return this.getDevices().find((d) => d.ieeeAddr === ieeeAddr);
}
getCoordinator() {
@@ -186,48 +186,40 @@ class Zigbee {
return this.shepherd.find(device.ieeeAddr, 1);
}
publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback) {
const device = this._findDevice(deviceID, ep);
publish(ieeAddr, cid, cmd, cmdType, zclData, cfg, ep, callback) {
const device = this._findDevice(ieeAddr, ep);
if (!device) {
logger.error(`Zigbee cannot publish message to device because '${deviceID}' not known by zigbee-shepherd`);
logger.error(`Zigbee cannot publish message to device because '${ieeAddr}' not known by zigbee-shepherd`);
return;
}
logger.info(
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ` +
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
);
const callback_ = (error) => {
const callback_ = (error, rsp) => {
if (error) {
logger.error(
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`- ${JSON.stringify(cfg)} - ${ep} ` +
`failed with error ${error}`);
}
callback(error);
callback(error, rsp);
// TODO: Send read reponse back to MQTT.
};
if (type === 'functional') {
if (cmdType === 'functional') {
device.functional(cid, cmd, zclData, cfg, callback_);
} else if (type === 'foundation') {
device.foundation(cid, cmd, [zclData], cfg, callback_);
} else if (cmdType === 'foundation') {
device.foundation(cid, cmd, zclData, cfg, callback_);
} else {
logger.error(`Unknown zigbee publish type ${type}`);
logger.error(`Unknown zigbee publish cmdType ${cmdType}`);
}
}
read(deviceID, cid, attr, ep, callback) {
const device = this._findDevice(deviceID, ep);
if (!device) {
logger.error(`Zigbee cannot read attribute from device because '${deviceID}' not known by zigbee-shepherd`);
return;
}
device.read(cid, attr, callback);
}
networkScan(callback) {
logger.info('Starting network scan...');
this.shepherd.lqiScan().then((result) => {
+460 -420
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -1,12 +1,15 @@
{
"name": "zigbee2mqtt",
"version": "0.1.8",
"version": "0.2.0",
"description": "Zigbee to MQTT bridge using zigbee-shepherd",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": ">=8.11 10"
},
"keywords": [
"xiaomi",
"tradfri",
@@ -38,11 +41,12 @@
"moment": "*",
"mqtt": "*",
"object-assign-deep": "*",
"queue": "*",
"rimraf": "*",
"winston": "2.4.2",
"zcl-packet": "git+https://github.com/Koenkk/zcl-packet.git#fbd8c936bbd4be0597ad3e934be0ca722b0128a6",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#c3fdef2c30267573aa5821a0f2c51f21a700eff2",
"zigbee-shepherd-converters": "4.0.7"
"zigbee-shepherd-converters": "5.0.1"
},
"devDependencies": {
"chai": "*",
@@ -52,6 +56,7 @@
"eslint-plugin-mocha": "*",
"mocha": "*",
"nyc": "*",
"proxyquire": "*"
"proxyquire": "*",
"sinon": "*"
}
}
+3
View File
@@ -63,7 +63,10 @@ text += ' will_message:\n';
text += ` topic: 'hass/status'\n`;
text += ` payload: 'offline'\n`;
text += '```';
text += '\n\n'
text += 'Zigbee2mqtt is expecting Home Assistant to send it\'s birth/will messages to `hass/status`.'
text += 'Be sure to add this to your `configuration.yaml` if you want zigbee2mqtt to resend the cached values when Home Assistant restarts'
text += '\n\n'
text += 'To respond to button clicks (e.g. WXKG01LM) you can use the following Home Assistant configuration:\n'
+165
View File
@@ -0,0 +1,165 @@
/* global describe, it */
const chai = require('chai');
const sinon = require('sinon');
const DevicePublish = require('../lib/extension/devicePublish');
const settings = require('../lib/util/settings');
const mqtt = {
subscribe: (topic) => {},
};
describe('DevicePublish', () => {
let devicePublish;
beforeEach(() => {
devicePublish = new DevicePublish(null, mqtt, null, null);
});
describe('Parse topic', () => {
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt1/my_device_id/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt1/my_device_id/sett';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt/my_device_id/write';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should parse set topic', () => {
const topic = 'zigbee2mqtt/my_device_id/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, 'my_device_id');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse get topic', () => {
const topic = 'zigbee2mqtt/my_device_id2/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.deviceID, 'my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse topic with when base topic has multiple slashes', () => {
const stub = sinon.stub(settings, 'get').callsFake(() => {
return {
mqtt: {
base_topic: 'zigbee2mqtt/at/my/home',
},
};
});
const topic = 'zigbee2mqtt/at/my/home/my_device_id2/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.deviceID, 'my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
stub.restore();
});
it('Should parse topic with when deviceID has multiple slashes', () => {
const topic = 'zigbee2mqtt/floor0/basement/my_device_id2/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, 'floor0/basement/my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse topic with when base and deviceID have multiple slashes', () => {
const stub = sinon.stub(settings, 'get').callsFake(() => {
return {
mqtt: {
base_topic: 'zigbee2mqtt/at/my/basement',
},
};
});
const topic = 'zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, 'floor0/basement/my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
stub.restore();
});
it('Should parse set with ieeAddr topic', () => {
const topic = 'zigbee2mqtt/0x12345689/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/left/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'left');
});
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/right/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'right');
});
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/bottom_left/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'bottom_left');
});
it('Shouldnt parse set with invalid postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/invalid/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689/invalid');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse set with and slashes in base and deviceID postfix topic', () => {
const stub = sinon.stub(settings, 'get').callsFake(() => {
return {
mqtt: {
base_topic: 'zigbee2mqtt/at/my/home',
},
};
});
const topic = 'zigbee2mqtt/at/my/home/my/device/in/basement/sensor/bottom_left/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.deviceID, 'my/device/in/basement/sensor');
chai.assert.strictEqual(parsed.postfix, 'bottom_left');
stub.restore();
});
});
});
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash -e
echo "Stopping zigbee2mqtt..."
sudo systemctl stop zigbee2mqtt
echo "Creating backup of configuration..."
cp -R data data-backup
echo "Updating..."
git checkout HEAD -- npm-shrinkwrap.json
git pull
echo "Installing dependencies..."
rm -rf node_modules
npm install
echo "Restore configuration..."
cp -R data-backup/* data
rm -rf data-backup
echo "Starting zigbee2mqtt..."
sudo systemctl start zigbee2mqtt
echo "Done!"