mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d49e136a6 | ||
|
|
0c1dba7208 | ||
|
|
63f4f681c4 | ||
|
|
1afbe05318 | ||
|
|
8376b7c5f6 | ||
|
|
b71a9920de | ||
|
|
2fd59cb7ef | ||
|
|
60e72c612c | ||
|
|
fbbc1fad73 | ||
|
|
d5de918247 | ||
|
|
f916ec5f4c | ||
|
|
469a01ebb6 | ||
|
|
6ede5a373e | ||
|
|
6e1af542e5 | ||
|
|
53051e9192 | ||
|
|
542a5f1564 | ||
|
|
b82f0e46aa | ||
|
|
f0f2dc847b | ||
|
|
8433571b1e | ||
|
|
2141d670bf | ||
|
|
fccb4de565 | ||
|
|
236afaacaf | ||
|
|
f463ca68a5 | ||
|
|
7502ba5312 | ||
|
|
c05b70b30b | ||
|
|
c22f8b36ba | ||
|
|
7276d116c2 | ||
|
|
1b7549dd73 | ||
|
|
7e4fff8454 | ||
|
|
ad43d61f28 | ||
|
|
ed5c52654f | ||
|
|
4f1b521bee | ||
|
|
a46f06a312 | ||
|
|
6311dfb0f1 | ||
|
|
ee82a885f7 | ||
|
|
88e6c10fb2 | ||
|
|
88524b7aed | ||
|
|
b2a27dd64a | ||
|
|
2a520b0e81 | ||
|
|
0c32f25203 | ||
|
|
1aa020875a | ||
|
|
9e5acdc94e | ||
|
|
417879b6a8 | ||
|
|
4ee7845b2a | ||
|
|
fb9600d55d | ||
|
|
47de3e20ae | ||
|
|
c00f091d7f | ||
|
|
b0b2bd2444 | ||
|
|
1e0c64caf5 | ||
|
|
db1fd6dd12 | ||
|
|
9ee0049248 | ||
|
|
7403fc7ce4 | ||
|
|
79d8cb69c0 |
+30
-9
@@ -3,13 +3,15 @@ const Zigbee = require('./zigbee');
|
||||
const State = require('./state');
|
||||
const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
const ExtensionNetworkMap = require('./extension/networkMap');
|
||||
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 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.
|
||||
@@ -94,6 +96,11 @@ class Controller {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize extensions.
|
||||
this.extensions = [
|
||||
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state),
|
||||
];
|
||||
|
||||
// Resend all cached states.
|
||||
this.sendAllCachedStates();
|
||||
}
|
||||
@@ -214,7 +221,15 @@ class Controller {
|
||||
// Zigbee message receieved, reset soft reset timeout.
|
||||
this.softResetTimeout(true);
|
||||
|
||||
logger.debug('Recieved zigbee message with data', JSON.stringify(message.data));
|
||||
// Log the message.
|
||||
let logMessage = `Recieved 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;
|
||||
logMessage += ` of device '${device.modelId}' (${device.ieeeAddr})`;
|
||||
}
|
||||
logger.debug(logMessage);
|
||||
|
||||
if (message.type == 'devInterview' && !settings.getDevice(message.data)) {
|
||||
logger.info('Connecting with device...');
|
||||
this.mqtt.log('pairing', 'connecting with device');
|
||||
@@ -319,7 +334,7 @@ class Controller {
|
||||
});
|
||||
|
||||
// Add device linkquality.
|
||||
if (message.linkquality) {
|
||||
if (message.hasOwnProperty('linkquality')) {
|
||||
payload.linkquality = message.linkquality;
|
||||
}
|
||||
|
||||
@@ -337,6 +352,12 @@ class Controller {
|
||||
handleMQTTMessage(topic, message) {
|
||||
logger.debug(`Recieved mqtt message on topic '${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));
|
||||
|
||||
if (topic.match(mqttConfigRegex)) {
|
||||
this.handleMQTTMessageConfig(topic, message);
|
||||
} else if (topic.match(mqttDeviceRegex) || topic.match(mqttDevicePrefixRegex)) {
|
||||
@@ -348,13 +369,13 @@ class Controller {
|
||||
clearTimeout(timer);
|
||||
}, 20000);
|
||||
}
|
||||
} else {
|
||||
} else if (!extensionResults.includes(true)) {
|
||||
logger.warn(`Cannot handle MQTT message with topic '${topic}' and message '${message}'`);
|
||||
}
|
||||
}
|
||||
|
||||
handleMQTTMessageConfig(topic, message) {
|
||||
const option = topic.split('/')[3];
|
||||
const option = topic.split('/').slice(-1)[0];
|
||||
|
||||
if (option === 'permit_join') {
|
||||
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true');
|
||||
@@ -458,8 +479,8 @@ class Controller {
|
||||
}
|
||||
|
||||
handleMQTTMessageDevice(topic, message, withPrefix) {
|
||||
const friendlyName = topic.split('/')[1];
|
||||
const topicPrefix = withPrefix ? topic.split('/')[2] : '';
|
||||
const friendlyName = topic.split('/').slice(-2)[0];
|
||||
const topicPrefix = withPrefix ? topic.split('/').slice(-3)[0] : '';
|
||||
|
||||
// Map friendlyName to deviceID.
|
||||
const deviceID = settings.getIDByFriendlyName(friendlyName);
|
||||
@@ -520,7 +541,7 @@ class Controller {
|
||||
}
|
||||
};
|
||||
|
||||
this.zigbee.publish(deviceID, message.cid, message.cmd, message.zclData, ep, callback);
|
||||
this.zigbee.publish(deviceID, message.cid, message.cmd, message.zclData, ep, message.type, callback);
|
||||
|
||||
published.push({message: message, converter: converter});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
const settings = require('../util/settings');
|
||||
|
||||
class NetworkMap {
|
||||
constructor(zigbee, mqtt, state) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
|
||||
// Subscribe to topic.
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
|
||||
this.mqtt.subscribe(this.topic);
|
||||
|
||||
// Set supported formats
|
||||
this.supportedFormats = {
|
||||
'raw': this.raw,
|
||||
'graphviz': this.graphviz,
|
||||
};
|
||||
}
|
||||
|
||||
handleMQTTMessage(topic, message) {
|
||||
message = message.toString();
|
||||
|
||||
if (topic === this.topic && this.supportedFormats.hasOwnProperty(message)) {
|
||||
this.zigbee.networkScan((result)=> {
|
||||
const converted = this.supportedFormats[message](result);
|
||||
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
raw(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`;
|
||||
});
|
||||
|
||||
text += '}';
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NetworkMap;
|
||||
@@ -46,6 +46,17 @@ const configurations = {
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'binary_sensor_gas': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'gas',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.gas }}',
|
||||
device_class: 'gas',
|
||||
json_attributes: [],
|
||||
},
|
||||
},
|
||||
'binary_sensor_router': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'router',
|
||||
@@ -106,6 +117,7 @@ const configurations = {
|
||||
icon: 'mdi:toggle-switch',
|
||||
value_template: '{{ value_json.click }}',
|
||||
json_attributes: ['battery', 'voltage', 'action', 'duration'],
|
||||
force_update: true,
|
||||
},
|
||||
},
|
||||
'sensor_power': {
|
||||
@@ -125,6 +137,7 @@ const configurations = {
|
||||
icon: 'mdi:gesture-double-tap',
|
||||
value_template: '{{ value_json.action }}',
|
||||
json_attributes: ['battery', 'voltage', 'angle', 'side', 'from_side', 'to_side', 'brightness'],
|
||||
force_update: true,
|
||||
},
|
||||
},
|
||||
'sensor_brightness': {
|
||||
@@ -137,6 +150,15 @@ const configurations = {
|
||||
json_attributes: [],
|
||||
},
|
||||
},
|
||||
'sensor_lock': {
|
||||
type: 'sensor',
|
||||
object_id: 'lock',
|
||||
discovery_payload: {
|
||||
icon: 'mdi:lock',
|
||||
value_template: '{{ value_json.inserted }}',
|
||||
json_attributes: ['forgotten', 'keyerror'],
|
||||
},
|
||||
},
|
||||
|
||||
// Light
|
||||
'light_brightness_colortemp_xy': {
|
||||
@@ -286,6 +308,10 @@ const mapping = {
|
||||
'8718696598283': [configurations.light_brightness_colortemp],
|
||||
'73693': [configurations.light_brightness_colortemp_xy],
|
||||
'324131092621': [configurations.sensor_action],
|
||||
'9290012607': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_illuminance,
|
||||
],
|
||||
'GL-C-008': [configurations.light_brightness_colortemp_xy],
|
||||
'STSS-MULT-001': [configurations.binary_sensor_contact],
|
||||
'E11-G23': [configurations.light_brightness],
|
||||
@@ -305,6 +331,16 @@ const mapping = {
|
||||
'8718696548738': [configurations.light_brightness_colortemp],
|
||||
'4052899926110': [configurations.light_brightness_colortemp_xy],
|
||||
'Z01-CIA19NAE26': [configurations.light_brightness],
|
||||
'E11-N1EA': [configurations.light_brightness_colortemp_xy],
|
||||
'74283': [configurations.light_brightness],
|
||||
'JTQJ-BF-01LM/BW': [configurations.binary_sensor_gas],
|
||||
'50045': [configurations.light_brightness],
|
||||
'AV2010/22': [configurations.binary_sensor_occupancy],
|
||||
'3210-L': [configurations.switch],
|
||||
'7299355PH': [configurations.light_brightness_colortemp_xy],
|
||||
'A6121': [configurations.sensor_lock],
|
||||
'433714': [configurations.light_brightness],
|
||||
'3261030P7': [configurations.light_brightness_colortemp],
|
||||
};
|
||||
|
||||
// A map of all discoverd devices
|
||||
|
||||
+10
-1
@@ -30,6 +30,11 @@ class MQTT {
|
||||
options.clientId = mqttSettings.client_id;
|
||||
}
|
||||
|
||||
if (mqttSettings.hasOwnProperty('reject_unauthorized') && !mqttSettings.reject_unauthorized) {
|
||||
logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`);
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
this.client = mqtt.connect(mqttSettings.server, options);
|
||||
|
||||
// Register callbacks.
|
||||
@@ -65,7 +70,11 @@ class MQTT {
|
||||
handleConnect() {
|
||||
logger.info('Connected to MQTT server');
|
||||
this.publish('bridge/state', 'online', {retain: true, qos: 0});
|
||||
this.subscriptions.forEach((topic) => this.client.subscribe(topic));
|
||||
this.subscriptions.forEach((topic) => this.subscribe(topic));
|
||||
}
|
||||
|
||||
subscribe(topic) {
|
||||
this.client.subscribe(topic);
|
||||
}
|
||||
|
||||
handleMessage(topic, message) {
|
||||
|
||||
+19
-26
@@ -1,6 +1,5 @@
|
||||
const winston = require('winston');
|
||||
const moment = require('moment');
|
||||
const data = require('./data');
|
||||
const settings = require('./settings');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@@ -8,21 +7,11 @@ const fx = require('mkdir-recursive');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
// Determine the log level.
|
||||
let level = winston.level.info;
|
||||
if (process.env.DEBUG) {
|
||||
level = 'debug';
|
||||
} else if (settings.get().advanced && settings.get().advanced.log_level) {
|
||||
level = settings.get().advanced.log_level;
|
||||
}
|
||||
const level = settings.get().advanced.log_level;
|
||||
|
||||
// Directoy to log to
|
||||
let rootDirectory = path.join(data.getPath(), 'log');
|
||||
if (settings.get().advanced && settings.get().advanced.log_directory) {
|
||||
rootDirectory = settings.get().advanced.log_directory;
|
||||
}
|
||||
|
||||
// Add start time to directory.
|
||||
const directory = path.join(rootDirectory, moment(Date.now()).format('YYYY-MM-DD.HH:mm:ss'));
|
||||
const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss');
|
||||
const directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
|
||||
|
||||
// Make sure that log directoy exsists
|
||||
fx.mkdirSync(directory);
|
||||
@@ -34,8 +23,8 @@ const logger = new (winston.Logger)({
|
||||
filename: path.join(directory, 'log.txt'),
|
||||
json: false,
|
||||
level: level,
|
||||
maxFiles: 3, // Max 3 files per run.
|
||||
maxsize: 10000000, // 10MB // Only if Filename is static!
|
||||
maxFiles: 3, // Keep last 3 files
|
||||
maxsize: 10000000, // 10MB
|
||||
timestamp: () => new Date().toLocaleString(),
|
||||
}),
|
||||
new (winston.transports.Console)({
|
||||
@@ -54,16 +43,20 @@ logger.info(`Logging to directory: '${directory}'`);
|
||||
logger.transports.console.level = level;
|
||||
|
||||
// Cleanup any old log directory.
|
||||
let directories = fs.readdirSync(rootDirectory).map((d) => {
|
||||
d = path.join(rootDirectory, d);
|
||||
return {path: d, birth: fs.statSync(d).birthtimeMs};
|
||||
});
|
||||
if (settings.get().advanced.log_directory.includes('%TIMESTAMP%')) {
|
||||
const rootDirectory = path.join(directory, '..');
|
||||
|
||||
directories.sort((a, b) => b.birth - a.birth);
|
||||
directories = directories.slice(10, directories.length);
|
||||
directories.forEach((dir) => {
|
||||
logger.debug(`Removing old log directory '${dir.path}'`);
|
||||
rimraf.sync(dir.path);
|
||||
});
|
||||
let directories = fs.readdirSync(rootDirectory).map((d) => {
|
||||
d = path.join(rootDirectory, d);
|
||||
return {path: d, birth: fs.statSync(d).birthtimeMs};
|
||||
});
|
||||
|
||||
directories.sort((a, b) => b.birth - a.birth);
|
||||
directories = directories.slice(10, directories.length);
|
||||
directories.forEach((dir) => {
|
||||
logger.debug(`Removing old log directory '${dir.path}'`);
|
||||
rimraf.sync(dir.path);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
|
||||
+10
-1
@@ -2,6 +2,15 @@ const yaml = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
const data = require('./data');
|
||||
const file = data.joinPath('configuration.yaml');
|
||||
const objectAssignDeep = require(`object-assign-deep`);
|
||||
const path = require('path');
|
||||
|
||||
const defaults = {
|
||||
advanced: {
|
||||
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
|
||||
log_level: process.env.DEBUG ? 'debug' : 'info',
|
||||
},
|
||||
};
|
||||
|
||||
let settings = read();
|
||||
|
||||
@@ -57,7 +66,7 @@ function changeFriendlyName(old, new_) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get: () => settings,
|
||||
get: () => objectAssignDeep.noMutate(defaults, settings),
|
||||
write: () => write(),
|
||||
getDevice: (id) => settings.devices ? settings.devices[id] : null,
|
||||
addDevice: (id) => addDevice(id),
|
||||
|
||||
+20
-3
@@ -183,7 +183,7 @@ class Zigbee {
|
||||
return this.shepherd.find(device.ieeeAddr, 1);
|
||||
}
|
||||
|
||||
publish(deviceID, cid, cmd, zclData, ep, callback) {
|
||||
publish(deviceID, cid, cmd, zclData, ep, type, callback) {
|
||||
const device = this._findDevice(deviceID, ep);
|
||||
if (!device) {
|
||||
logger.error(`Zigbee cannot publish message to device because '${deviceID}' not known by zigbee-shepherd`);
|
||||
@@ -191,7 +191,8 @@ class Zigbee {
|
||||
}
|
||||
|
||||
logger.info(`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} - ${ep}`);
|
||||
device.functional(cid, cmd, zclData, (error) => {
|
||||
|
||||
const callback_ = (error) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} - ${ep} ` +
|
||||
@@ -199,7 +200,15 @@ class Zigbee {
|
||||
}
|
||||
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
if (type === 'functional') {
|
||||
device.functional(cid, cmd, zclData, callback_);
|
||||
} else if (type === 'foundation') {
|
||||
device.foundation(cid, cmd, [zclData], callback_);
|
||||
} else {
|
||||
logger.error(`Unknown zigbee publish type ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
read(deviceID, cid, attr, ep, callback) {
|
||||
@@ -212,6 +221,14 @@ class Zigbee {
|
||||
device.read(cid, attr, callback);
|
||||
}
|
||||
|
||||
networkScan(callback) {
|
||||
logger.info('Starting network scan...');
|
||||
this.shepherd.lqiScan().then((result) => {
|
||||
logger.info('Network scan completed');
|
||||
callback(result);
|
||||
});
|
||||
}
|
||||
|
||||
registerOnAfIncomingMsg(ieeeAddr, ep) {
|
||||
const device = this._findDevice(ieeeAddr, ep);
|
||||
device.onAfIncomingMsg = (message) => {
|
||||
|
||||
Generated
+501
-1410
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.5",
|
||||
"description": "Zigbee to MQTT bridge using zigbee-shepherd",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -40,8 +40,8 @@
|
||||
"object-assign-deep": "*",
|
||||
"rimraf": "*",
|
||||
"winston": "2.4.2",
|
||||
"zcl-packet": "git+https://github.com/Koenkk/zcl-packet.git#b7d5b4478a88cd3fc65328c6dc94572c277c7d3e",
|
||||
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#7673fc5a285dc93d2e466bc2aa7134220e1fb134",
|
||||
"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": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user