mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0d3c2f964 | ||
|
|
f797a12250 | ||
|
|
081d3c3dfe | ||
|
|
7ca966ce39 | ||
|
|
fc664bdc68 | ||
|
|
2cb27b14f9 | ||
|
|
86a102e081 | ||
|
|
9119e54c2c | ||
|
|
f379bc0391 | ||
|
|
2e736bf501 | ||
|
|
1e7f985d4e | ||
|
|
61c4ff33e0 | ||
|
|
73ae48f387 | ||
|
|
0a1c60b2cf | ||
|
|
4eb56319c4 | ||
|
|
d8ea36af29 | ||
|
|
d0cf61bf10 | ||
|
|
ddb83cc6e4 | ||
|
|
e54de4bea7 |
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"env": {
|
||||
"mocha": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
|
||||
+2
-1
@@ -62,4 +62,5 @@ data/database.db
|
||||
data/config.json
|
||||
data/log*.txt
|
||||
data/state.json
|
||||
data/log
|
||||
data/log
|
||||
data-backup/
|
||||
+1
-1
@@ -6,7 +6,7 @@ services:
|
||||
- docker
|
||||
|
||||
node_js:
|
||||
- "8"
|
||||
- "10"
|
||||
|
||||
install:
|
||||
- npm install
|
||||
|
||||
@@ -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
|
||||

|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -30,6 +30,8 @@ class NetworkMap {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
raw(zigbee, topology) {
|
||||
|
||||
+44
-22
@@ -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
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
Generated
+460
-420
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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!"
|
||||
Reference in New Issue
Block a user