Compare commits

..
56 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
Koenkk 97cc56b691 0.1.8 2018-10-19 22:44:26 +02:00
Koenkk c9df5c8221 Update zigbee-shepherd-converters to 4.0.7. 2018-10-19 22:27:29 +02:00
dcshoes23 7f13e2c3e6 Add homeassistant auto-discovery support for CE LED potlight 4" (#505) 2018-10-19 22:20:11 +02:00
Koenkk 0aab565083 Update zigbee-shepherd-converters to 4.0.6 2018-10-17 21:17:45 +02:00
Kiall Mac Innes 153f8bcfb5 Add Philips Hue white ambiance E14 (LTW012) HA Support (#491)
Adds HA support for LTW012 devices - See https://github.com/Koenkk/zigbee-shepherd-converters/pull/100
2018-10-17 18:25:33 +02:00
Simon van der Veldt c04ed81adb Fix typo in logging messages (#486) 2018-10-16 17:51:54 +02:00
Koen Kanters a1cf75456b Fix state caching. #485 2018-10-16 17:34:46 +02:00
Koenkk b063e67e24 Update zigbee-shepherd-converters to 4.0.5. https://github.com/Koenkk/zigbee2mqtt/issues/467 2018-10-15 19:07:12 +02:00
Koenkk a4da482225 0.1.7 2018-10-15 17:37:06 +02:00
Koenkk 98302bebd1 Update zigbee-shepherd-converters to 4.0.3 2018-10-09 21:28:32 +02:00
AnhDuc85 2197328db5 add homeassistant config for Osram Outdoor Lantern W RGBW OSRAM (#464) 2018-10-09 20:47:29 +02:00
Koen Kanters c5b9176614 Fix unique_id being set when not all devices have a friendly_name. 2018-10-08 20:25:04 +02:00
Laurent 0322354c37 [RFC] graphviz network map, display all devices (#443)
* network map, more info

* add a get(All)Devices method to zigbee

* graphviz: display all devices

loop through all devices, display all devices even those that haven't
responded to the lqi scan.

* makes eslint happy :)

* remove null chars from network map graphviz output

makes graphviz happy :-)

* Improvements to graphviz network map

* Always add device type
2018-10-07 21:46:54 +02:00
Koenkk b692c94fe9 Update zigbee-shepherd. https://github.com/Koenkk/zigbee-shepherd/pull/6 2018-10-05 22:08:47 +02:00
Koenkk 1496861811 Fix https://github.com/Koenkk/zigbee2mqtt/issues/373 again. 2018-10-05 20:54:57 +02:00
Koen Kanters 5459152f01 Fix Koenkk. #451 2018-10-05 19:35:23 +02:00
Koenkk 1db926173d Refactor router polling & only poll Xiaomi routers, refactor soft reset timeout and disable by default. #274 2018-10-02 21:15:12 +02:00
Koenkk da9ee71d80 Fix crash when deviceSettings are missing. 2018-10-02 20:35:14 +02:00
Phil Hawthorne d9f02230a0 Add autodiscovery support for Sylvania MR16 (#435)
Adds auto discovery support for the MR16 smart bulbs from
Osram/Sylvania.
2018-10-01 16:39:23 +02:00
Koenkk bf68f4575f Update zigbee-shepherd-converters to 4.0.2 2018-09-29 23:05:18 +02:00
Koenkk 33ea08ad22 Update dependencies for. #388 2018-09-29 23:05:18 +02:00
UnrealKazu 3a17db7c79 Add support for TRADFRI Control Outlet (#413)
The control outlet is a switch, hence the switch configuration.
2018-09-29 23:05:18 +02:00
Koenkk e55252b9ff Mock state ON when moveToLevelWithOnOff succeeds. https://github.com/Koenkk/zigbee-shepherd-converters/pull/81 2018-09-29 23:05:18 +02:00
Koenkk 71d12c3c71 Revert "Allow joining through specified device."
This reverts commit bf85ea1494.
2018-09-29 23:05:18 +02:00
Koenkk bc7b2bd5e0 Allow joining through specified device. 2018-09-29 23:05:18 +02:00
Koenkk 50d58d3dff Always call zigbee shepherd permit join (in both true and false). 2018-09-29 23:05:18 +02:00
Koenkk 4180bd8933 Add friendly_name to graphviz network map. #252 2018-09-29 23:05:18 +02:00
Koenkk 172d11677b Update zigbee-shepherd-converters. 2018-09-29 23:05:18 +02:00
Plácido Revilla ab5537e18c Add support for GE 45857 in-wall smart dimmer (#394)
* Add support for homeassistant discovery of E11-N1EA

* Add support for GE 45857 in-wall smart dimmer
2018-09-29 23:05:18 +02:00
Viet Dzung b06e3b61f2 Support cfg in foundation and functional (#390)
* Support cfg in foundation and functional

* Update zigbee-shepherd-converters.
2018-09-29 23:05:18 +02:00
Stanislav Demydiuk 03d913defc Add ability to send device information in MQTT message payload (#374) 2018-09-29 23:05:18 +02:00
Koenkk e4bcac5411 Update zigbee-shepherd. https://github.com/Koenkk/zigbee-shepherd/pull/4 2018-09-29 23:05:18 +02:00
Koenkk 5ed29d045b 0.1.6 2018-09-23 11:21:24 +02:00
Koen Kanters 75acffa8c7 Fix topicPrefix parsing. #387 2018-09-21 11:12:51 +02:00
Koenkk ade84dc6c0 Fix MQTT command with prefix, introduced in #c22f8b3. #387 2018-09-20 16:16:57 +02:00
Koenkk 7d49e136a6 0.1.5 2018-09-19 21:00:03 +02:00
Koenkk 0c1dba7208 Fix functional command. #386 2018-09-19 20:59:30 +02:00
22 changed files with 1281 additions and 878 deletions
+3 -1
View File
@@ -1,10 +1,12 @@
{
"env": {
"mocha": true,
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "google"],
"parserOptions": {
"ecmaVersion": 6,
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
+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
+6 -1
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)
@@ -39,6 +44,6 @@ If you need assistance you can check [opened issues](https://github.com/Koenkk/z
* [AndrewLinden](https://github.com/AndrewLinden)
* [oskarn97](https://github.com/oskarn97)
* [dgomes](https://github.com/dgomes)
* [Koenkk](https://github.com/Koenk)
* [Koenkk](https://github.com/Koenkk)
* [kirovilya](https://github.com/kirovilya)
* [ciotlosm](https://github.com/ciotlosm)
+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();
+59 -184
View File
@@ -4,17 +4,14 @@ const State = require('./state');
const logger = require('./util/logger');
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 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 pollInterval = 60 * 1000; // seconds * 1000.
const softResetTimeout = 3600 * 1000; // seconds * 1000.
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
@@ -33,17 +30,19 @@ if (settings.get().homeassistant && !cacheState) {
class Controller {
constructor() {
this.zigbee = new Zigbee();
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
this.zigbee = new Zigbee(this.handleZigbeeMessage);
this.mqtt = new MQTT();
this.state = new State();
this.configured = [];
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
this.extensions = [];
}
start() {
this.startupLogVersion(() => {
this.zigbee.start(this.handleZigbeeMessage, (error) => {
this.zigbee.start((error) => {
if (error) {
logger.error('Failed to start', error);
} else {
@@ -60,17 +59,12 @@ class Controller {
logger.warn('`permit_join` set to `true` in configuration.yaml.');
logger.warn('Allowing new devices to join.');
logger.warn('Set `permit_join` to `false` once you joined all devices.');
this.zigbee.permitJoin(true);
}
// Start timers.
this.pollTimer(true);
this.softResetTimeout(true);
this.zigbee.permitJoin(settings.get().permit_join);
// 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/+`,
];
@@ -98,7 +92,10 @@ class Controller {
// Initialize extensions.
this.extensions = [
new ExtensionNetworkMap(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.
@@ -108,62 +105,15 @@ class Controller {
sendAllCachedStates() {
this.zigbee.getAllClients().forEach((device) => {
if (this.state.exists(device.ieeeAddr)) {
this.mqttPublishDeviceState(device.ieeeAddr, this.state.get(device.ieeeAddr), false);
this.mqttPublishDeviceState(device, this.state.get(device.ieeeAddr), false);
}
});
}
softResetTimeout(start) {
if (this._softResetTimer) {
clearTimeout(this._softResetTimer);
this._softResetTimer = null;
}
if (start) {
this._softResetTimer = setTimeout(() => {
this.zigbee.softReset((error) => {
if (error) {
logger.warn('Soft reset error', error);
this.zigbee.stop((error) => {
logger.warn('Zigbee stopped');
this.zigbee.start(this.handleZigbeeMessage, (error) => {
if (error) {
logger.error('Failed to restart!');
}
});
});
} else {
logger.warn('Soft resetted zigbee');
}
this.softResetTimeout(true);
});
}, softResetTimeout);
}
}
pollTimer(start) {
// Some routers need polling to prevent them from sleeping.
if (start && !this._pollTimer) {
this._pollTimer = setInterval(() => {
const devices = this.zigbee.getAllClients().filter((d) => {
const power = d.powerSource ? d.powerSource.toLowerCase().split(' ')[0] : 'unknown';
return power !== 'battery' && power !== 'unknown' && d.type === 'Router';
});
devices.forEach((d) => this.zigbee.ping(d.ieeeAddr));
}, pollInterval);
} else if (!start && this._pollTimer) {
clearTimeout(this._pollTimer);
this._pollTimer = null;
}
}
stop(callback) {
this.extensions.filter((e) => e.stop).forEach((e) => e.stop());
this.state.save();
this.mqtt.disconnect();
this.pollTimer(false);
this.softResetTimeout(false);
this.zigbee.stop(callback);
}
@@ -217,12 +167,29 @@ class Controller {
`${friendlyDevice.vendor} ${friendlyDevice.description} (${type})`;
}
getDeviceInfoForMqtt(device) {
const {type, ieeeAddr, nwkAddr, manufId, manufName, powerSource, modelId, status} = device;
const deviceSettings = settings.getDevice(device.ieeeAddr);
return {
ieeeAddr,
friendlyName: deviceSettings.friendly_name || '',
type,
nwkAddr,
manufId,
manufName,
powerSource,
modelId,
status,
};
}
handleZigbeeMessage(message) {
// Zigbee message receieved, reset soft reset timeout.
this.softResetTimeout(true);
// Call extensions.
this.extensions.filter((e) => e.handleZigbeeMessage).forEach((e) => e.handleZigbeeMessage(message));
// Log the message.
let logMessage = `Recieved zigbee message of type '${message.type}' ` +
let logMessage = `Received zigbee message of type '${message.type}' ` +
`with data '${JSON.stringify(message.data)}'`;
if (message.endpoints && message.endpoints[0].device) {
const device = message.endpoints[0].device;
@@ -338,7 +305,7 @@ class Controller {
payload.linkquality = message.linkquality;
}
this.mqttPublishDeviceState(device.ieeeAddr, payload, cache);
this.mqttPublishDeviceState(device, payload, cache);
};
const payload = converter.convert(mappedModel, message, publish, settings.getDevice(device.ieeeAddr));
@@ -350,18 +317,15 @@ class Controller {
}
handleMQTTMessage(topic, message) {
logger.debug(`Recieved 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(() => {
@@ -369,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}'`);
}
}
@@ -478,124 +442,35 @@ class Controller {
}
}
handleMQTTMessageDevice(topic, message, withPrefix) {
const friendlyName = topic.split('/').slice(-2)[0];
const topicPrefix = withPrefix ? topic.split('/').slice(-3)[0] : '';
mqttPublishDeviceState(device, payload, cache) {
const deviceID = device.ieeeAddr;
const appSettings = settings.get();
let messagePayload = {...payload};
// 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')) {
const msg = {};
const _key = topicPrefix ? `${key}_${topicPrefix}` : key;
msg[_key] = json[key];
this.mqttPublishDeviceState(deviceID, msg, true);
}
};
this.zigbee.publish(deviceID, message.cid, message.cmd, message.zclData, 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(deviceID, payload, cache) {
if (cacheState) {
// Add cached state to payload
if (this.state.exists(deviceID)) {
payload = objectAssignDeep.noMutate(this.state.get(deviceID), payload);
messagePayload = objectAssignDeep.noMutate(this.state.get(deviceID), payload);
}
// Update state cache with new state.
if (cache) {
this.state.set(deviceID, payload);
this.state.set(deviceID, messagePayload);
}
}
const deviceSettings = settings.getDevice(deviceID);
const friendlyName = deviceSettings ? deviceSettings.friendly_name : deviceID;
const options = {
retain: deviceSettings.retain,
qos: deviceSettings.qos ? deviceSettings.qos : 0,
retain: deviceSettings ? deviceSettings.retain : false,
qos: deviceSettings && deviceSettings.qos ? deviceSettings.qos : 0,
};
this.mqtt.publish(deviceSettings.friendly_name, JSON.stringify(payload), options);
if (appSettings.mqtt.include_device_information) {
messagePayload.device = this.getDeviceInfoForMqtt(device);
}
this.mqtt.publish(friendlyName, JSON.stringify(messagePayload), options);
}
startupLogVersion(callback) {
+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;
+46 -8
View File
@@ -1,5 +1,6 @@
const settings = require('../util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
class NetworkMap {
constructor(zigbee, mqtt, state) {
@@ -23,28 +24,65 @@ class NetworkMap {
if (topic === this.topic && this.supportedFormats.hasOwnProperty(message)) {
this.zigbee.networkScan((result)=> {
const converted = this.supportedFormats[message](result);
const converted = this.supportedFormats[message](this.zigbee, result);
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
});
return true;
}
return false;
}
raw(topology) {
raw(zigbee, topology) {
return JSON.stringify(topology);
}
graphviz(topology) {
let text = 'digraph G {\n';
topology.forEach((item) => {
text += ` "${item.ieeeAddr}" [label="${item.ieeeAddr} (${item.status})"];\n`;
text += ` "${item.ieeeAddr}" -> "${item.parent}" [label="${item.lqi}"]\n`;
graphviz(zigbee, topology) {
let text = 'digraph G {\nnode[shape=record];\n';
const lqiDevices = new Map(topology.map((d) => [d.ieeeAddr, d]));
zigbee.getDevices().forEach((device) => {
const labels = [];
const friendlyDevice = settings.getDevice(device.ieeeAddr);
const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
// Add friendly name
labels.push(friendlyName);
// Add the device type
labels.push(device.type);
// Add the device model
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
labels.push(`${mappedModel.vendor} ${mappedModel.description} (${mappedModel.model})`);
} else {
// This model is not supported by zigbee-shepherd-converters, add zigbee model information, if available
const zigbeeModel = [device.manufName, device.modelId].filter((a) => a).join(' ');
labels.push(zigbeeModel ? zigbeeModel : 'No model information available');
}
// Add the device status (online/offline)
labels.push(device.status);
// Add the device with its labels to the graph as a node.
text += ` "${device.ieeeAddr}" [label="{${labels.join('|')}}"];\n`;
/**
* Add an edge between the device and its parent to the graph
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
*/
const lqiDevice = lqiDevices.get(device.ieeeAddr);
if (lqiDevice != undefined) {
text += ` "${device.ieeeAddr}" -> "${lqiDevice.parent}" [label="${lqiDevice.lqi}"]\n`;
}
});
text += '}';
return text;
return text.replace(/\0/g, '');
}
}
+40
View File
@@ -0,0 +1,40 @@
const utils = require('../util/utils');
const interval = utils.secondsToMilliseconds(60);
/**
* This extensions polls Xiaomi Zigbee routers to keep them awake.
*/
class RouterPollXiaomi {
constructor(zigbee, mqtt, state) {
this.zigbee = zigbee;
this.timer = null;
this.startTimer();
}
startTimer() {
this.clearTimer();
this.timer = setInterval(() => this.handleInterval(), interval);
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
stop() {
this.clearTimer();
}
handleInterval() {
this.zigbee.getAllClients()
.filter((d) => utils.isXiaomiDevice(d)) // Filter Xiaomi devices
.filter((d) => d.type === 'Router') // Filter routers
.filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices
.forEach((d) => this.zigbee.ping(d.ieeeAddr)); // Ping devices.
}
}
module.exports = RouterPollXiaomi;
+66
View File
@@ -0,0 +1,66 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const utils = require('../util/utils');
/**
* This extensions soft resets the ZNP after a certain timeout.
*/
class SoftReset {
constructor(zigbee, mqtt, state) {
this.zigbee = zigbee;
this.timer = null;
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
if (this.timeout === 0) {
logger.debug(`Soft reset timeout disabled`);
} else {
logger.debug(`Soft reset timeout set to ${utils.millisecondsToSeconds(this.timeout)} seconds`);
}
this.resetTimer();
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
resetTimer() {
if (this.timeout === 0) {
return;
}
this.clearTimer();
this.timer = setTimeout(() => this.handleTimeout(), this.timeout);
}
handleTimeout() {
logger.warn('Soft reset timeout triggered');
this.zigbee.softReset((error) => {
if (error) {
logger.warn('Soft reset failed, trying stop/start');
this.zigbee.stop((error) => {
logger.warn('Zigbee stopped');
this.zigbee.start((error) => {
if (error) {
logger.error('Failed to restart!');
}
});
});
} else {
logger.warn('Soft resetted ZNP due to timeout');
}
this.resetTimer();
});
}
handleZigbeeMessage(message) {
this.resetTimer();
}
}
module.exports = SoftReset;
+64 -25
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: [],
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': {
@@ -136,7 +147,10 @@ const configurations = {
discovery_payload: {
icon: 'mdi:gesture-double-tap',
value_template: '{{ value_json.action }}',
json_attributes: ['battery', 'voltage', 'angle', 'side', 'from_side', 'to_side', 'brightness'],
json_attributes: [
'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,
},
},
@@ -147,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': {
@@ -156,7 +170,7 @@ const configurations = {
discovery_payload: {
icon: 'mdi:lock',
value_template: '{{ value_json.inserted }}',
json_attributes: ['forgotten', 'keyerror'],
json_attributes: ['linkquality', 'forgotten', 'keyerror'],
},
},
@@ -225,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}`],
},
};
};
@@ -262,6 +276,7 @@ const mapping = {
'PLUG EDP RE:DY': [configurations.switch, configurations.sensor_power],
'CC2530.ROUTER': [configurations.binary_sensor_router],
'AA70155': [configurations.light_brightness_colortemp],
'4058075816718': [configurations.light_brightness_colortemp_xy],
'AA69697': [configurations.light_brightness_colortemp_xy],
'HALIGHTDIMWWE27': [configurations.light_brightness],
'AB3257001NJ': [configurations.switch],
@@ -306,6 +321,7 @@ const mapping = {
'AB32840': [configurations.light_brightness_colortemp],
'8718696485880': [configurations.light_brightness_colortemp_xy],
'8718696598283': [configurations.light_brightness_colortemp],
'8718696695203': [configurations.light_brightness_colortemp],
'73693': [configurations.light_brightness_colortemp_xy],
'324131092621': [configurations.sensor_action],
'9290012607': [
@@ -314,8 +330,8 @@ const mapping = {
],
'GL-C-008': [configurations.light_brightness_colortemp_xy],
'STSS-MULT-001': [configurations.binary_sensor_contact],
'E11-G23': [configurations.light_brightness],
'AC03845': [configurations.light_brightness_colortemp_xy],
'E11-G23/E11-G33': [configurations.light_brightness],
'AC03645': [configurations.light_brightness_colortemp_xy],
'AC03641': [configurations.light_brightness],
'FB56+ZSW05HG1.2': [configurations.switch],
'72922-A': [configurations.switch],
@@ -337,10 +353,33 @@ const mapping = {
'50045': [configurations.light_brightness],
'AV2010/22': [configurations.binary_sensor_occupancy],
'3210-L': [configurations.switch],
'7299355PH': [configurations.light_brightness_colortemp_xy],
'7299355PH': [configurations.light_brightness_xy],
'45857GE': [configurations.light_brightness],
'A6121': [configurations.sensor_lock],
'433714': [configurations.light_brightness],
'3261030P7': [configurations.light_brightness_colortemp],
'DJT11LM': [configurations.sensor_action],
'E1603': [configurations.switch],
'7199960PH': [configurations.light_brightness_xy],
'74696': [configurations.light_brightness],
'AB35996': [configurations.light_brightness_colortemp_xy],
'AB401130055': [configurations.light_brightness_colortemp],
'74282': [configurations.light_brightness_colortemp],
'RS 128 T': [configurations.light_brightness_colortemp],
'53170161': [configurations.light_brightness_colortemp],
'4058075036147': [configurations.light_brightness_colortemp_xy],
'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
@@ -357,7 +396,7 @@ function discover(deviceID, model, mqtt, force) {
mapping[model].forEach((config) => {
const topic = `${config.type}/${deviceID}/${config.object_id}/config`;
const payload = config.discovery_payload;
const payload = {...config.discovery_payload};
payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
+3
View File
@@ -6,9 +6,11 @@ const objectAssignDeep = require(`object-assign-deep`);
const path = require('path');
const defaults = {
permit_join: false,
advanced: {
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
log_level: process.env.DEBUG ? 'debug' : 'info',
soft_reset_timeout: 0,
},
};
@@ -73,4 +75,5 @@ module.exports = {
removeDevice: (id) => removeDevice(id),
getIDByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
changeFriendlyName: (old, new_) => changeFriendlyName(old, new_),
getIeeAddrByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
};
+8
View File
@@ -0,0 +1,8 @@
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
const xiaomiManufacturerID = [4151, 4447];
module.exports = {
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
secondsToMilliseconds: (seconds) => seconds * 1000,
isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId),
};
+36 -29
View File
@@ -3,6 +3,7 @@ const logger = require('./util/logger');
const settings = require('./util/settings');
const data = require('./util/data');
const zclPacket = require('zcl-packet');
const utils = require('./util/utils');
const advancedSettings = settings.get().advanced;
@@ -21,13 +22,14 @@ const shepherdSettings = {
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`);
class Zigbee {
constructor() {
constructor(onMessage) {
this.onMessage = onMessage;
this.handleReady = this.handleReady.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.handleError = this.handleError.bind(this);
}
start(onMessage, callback) {
start(callback) {
logger.info(`Starting zigbee-shepherd`);
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
@@ -62,8 +64,6 @@ class Zigbee {
this.shepherd.on('ready', this.handleReady);
this.shepherd.on('ind', this.handleMessage);
this.shepherd.on('error', this.handleError);
this.onMessage = onMessage;
}
_logStartupInfo() {
@@ -84,12 +84,11 @@ class Zigbee {
}
handleReady() {
// Set all Xiaomi devices (manufId === 4151) to be online, so shepherd won't try
// to query info from devices (which would fail because they go tosleep).
// Xiaomi lumi.plug has manufId === 4447 and can be in the sleep mode too
// Set all Xiaomi devices to be online, so shepherd won't try
// to query info from devices (which would fail because they go to sleep).
const devices = this.getAllClients();
devices.forEach((d) => {
if ((d.manufId === 4151) || (d.manufId === 4447)) {
if (utils.isXiaomiDevice(d)) {
const device = this.shepherd.find(d.ieeeAddr, 1);
if (device) {
device.getDevice().update({
@@ -100,7 +99,7 @@ class Zigbee {
}
});
// Check if we have to turn of the led
// Check if we have to turn off the led
if (settings.get().serial.disable_led) {
this.shepherd.controller.request('UTIL', 'ledControl', {ledid: 3, mode: 0});
}
@@ -128,7 +127,7 @@ class Zigbee {
}
getAllClients() {
return this.shepherd.list().filter((device) => device.type !== 'Coordinator');
return this.getDevices().filter((device) => device.type !== 'Coordinator');
}
removeDevice(deviceID, callback) {
@@ -174,43 +173,51 @@ class Zigbee {
}
}
getDevice(deviceID) {
return this.shepherd.list().find((d) => d.ieeeAddr === deviceID);
getDevices() {
return this.shepherd.list();
}
getDevice(ieeeAddr) {
return this.getDevices().find((d) => d.ieeeAddr === ieeeAddr);
}
getCoordinator() {
const device = this.shepherd.list().find((d) => d.type === 'Coordinator');
const device = this.getDevices().find((d) => d.type === 'Coordinator');
return this.shepherd.find(device.ieeeAddr, 1);
}
publish(deviceID, cid, cmd, zclData, 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} - ${JSON.stringify(zclData)} - ${ep}`);
logger.info(
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
);
device[type](cid, cmd, [zclData], (error) => {
const callback_ = (error, rsp) => {
if (error) {
logger.error(
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} - ${ep} ` +
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`- ${JSON.stringify(cfg)} - ${ep} ` +
`failed with error ${error}`);
}
callback(error);
});
}
callback(error, rsp);
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;
// TODO: Send read reponse back to MQTT.
};
if (cmdType === 'functional') {
device.functional(cid, cmd, zclData, cfg, callback_);
} else if (cmdType === 'foundation') {
device.foundation(cid, cmd, zclData, cfg, callback_);
} else {
logger.error(`Unknown zigbee publish cmdType ${cmdType}`);
}
device.read(cid, attr, callback);
}
networkScan(callback) {
+582 -616
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -1,12 +1,15 @@
{
"name": "zigbee2mqtt",
"version": "0.1.4",
"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#8f17e5540946f9be198d737a879e334c24dbf20d",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#c096ed6719a6c941217c560b4cdcc04e4dd34c3b",
"zigbee-shepherd-converters": "*"
"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": "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!"