Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
790 additions
and
606 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,6 @@ install: | |
|
||
before_script: | ||
- npm test | ||
- npm run verify-homeassistant-mapping | ||
- npm run eslint | ||
|
||
script: | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
const settings = require('../util/settings'); | ||
const logger = require('../util/logger'); | ||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); | ||
|
||
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g'); | ||
const allowedLogLevels = ['error', 'warn', 'info', 'debug']; | ||
|
||
class BridgeConfig { | ||
constructor(zigbee, mqtt, state, publishDeviceState) { | ||
this.zigbee = zigbee; | ||
this.mqtt = mqtt; | ||
this.state = state; | ||
this.publishDeviceState = publishDeviceState; | ||
|
||
// Bind functions | ||
this.permitJoin = this.permitJoin.bind(this); | ||
this.logLevel = this.logLevel.bind(this); | ||
this.devices = this.devices.bind(this); | ||
this.rename = this.rename.bind(this); | ||
this.remove = this.remove.bind(this); | ||
|
||
// Set supported options | ||
this.supportedOptions = { | ||
'permit_join': this.permitJoin, | ||
'log_level': this.logLevel, | ||
'devices': this.devices, | ||
'rename': this.rename, | ||
'remove': this.remove, | ||
}; | ||
} | ||
|
||
permitJoin(topic, message) { | ||
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true'); | ||
} | ||
|
||
logLevel(topic, message) { | ||
const level = message.toString().toLowerCase(); | ||
if (allowedLogLevels.includes(level)) { | ||
logger.info(`Switching log level to '${level}'`); | ||
logger.transports.console.level = level; | ||
logger.transports.file.level = level; | ||
} else { | ||
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`); | ||
} | ||
} | ||
|
||
devices(topic, message) { | ||
const devices = this.zigbee.getAllClients().map((device) => { | ||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); | ||
const friendlyDevice = settings.getDevice(device.ieeeAddr); | ||
|
||
return { | ||
ieeeAddr: device.ieeeAddr, | ||
type: device.type, | ||
model: mappedDevice ? mappedDevice.model : device.modelId, | ||
friendly_name: friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr, | ||
}; | ||
}); | ||
|
||
this.mqtt.log('devices', devices); | ||
} | ||
|
||
rename(topic, message) { | ||
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` + | ||
`got ${message.toString()}`; | ||
|
||
let json = null; | ||
try { | ||
json = JSON.parse(message.toString()); | ||
} catch (e) { | ||
logger.error(invalid); | ||
return; | ||
} | ||
|
||
// Validate message | ||
if (!json.new || !json.old) { | ||
logger.error(invalid); | ||
return; | ||
} | ||
|
||
if (settings.changeFriendlyName(json.old, json.new)) { | ||
logger.info(`Successfully renamed - ${json.old} to ${json.new} `); | ||
} else { | ||
logger.error(`Failed to renamed - ${json.old} to ${json.new}`); | ||
return; | ||
} | ||
} | ||
|
||
remove(topic, message) { | ||
message = message.toString(); | ||
const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message); | ||
const deviceID = IDByFriendlyName ? IDByFriendlyName : message; | ||
const device = this.zigbee.getDevice(deviceID); | ||
|
||
const cleanup = () => { | ||
// Remove from configuration.yaml | ||
settings.removeDevice(deviceID); | ||
|
||
// Remove from state | ||
this.state.remove(deviceID); | ||
|
||
logger.info(`Successfully removed ${deviceID}`); | ||
this.mqtt.log('device_removed', message); | ||
}; | ||
|
||
// Remove from zigbee network. | ||
if (device) { | ||
this.zigbee.removeDevice(deviceID, (error) => { | ||
if (!error) { | ||
cleanup(); | ||
} else { | ||
logger.error(`Failed to remove ${deviceID}`); | ||
} | ||
}); | ||
} else { | ||
cleanup(); | ||
} | ||
} | ||
|
||
onMQTTConnected() { | ||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`); | ||
} | ||
|
||
onMQTTMessage(topic, message) { | ||
if (!topic.match(configRegex)) { | ||
return false; | ||
} | ||
|
||
const option = topic.split('/').slice(-1)[0]; | ||
|
||
if (!this.supportedOptions.hasOwnProperty(option)) { | ||
return false; | ||
} | ||
|
||
this.supportedOptions[option](topic, message); | ||
|
||
return true; | ||
} | ||
} | ||
|
||
module.exports = BridgeConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
const settings = require('../util/settings'); | ||
const logger = require('../util/logger'); | ||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); | ||
|
||
/** | ||
* This extensions handles configuration of devices. | ||
*/ | ||
class DeviceConfigure { | ||
constructor(zigbee, mqtt, state, publishDeviceState) { | ||
this.zigbee = zigbee; | ||
this.configured = []; | ||
} | ||
|
||
onZigbeeStarted() { | ||
this.zigbee.getAllClients().forEach((device) => { | ||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); | ||
|
||
if (mappedDevice) { | ||
this.configure(device, mappedDevice); | ||
} | ||
}); | ||
} | ||
|
||
onZigbeeMessage(message, device, mappedDevice) { | ||
if (device && mappedDevice) { | ||
this.configure(device, mappedDevice); | ||
} | ||
} | ||
|
||
configure(device, mappedDevice) { | ||
const ieeeAddr = device.ieeeAddr; | ||
|
||
if (!this.configured.includes(ieeeAddr) && mappedDevice.configure) { | ||
const friendlyName = settings.getDevice(ieeeAddr) ? settings.getDevice(ieeeAddr).friendly_name : 'unknown'; | ||
|
||
// Call configure function of this device. | ||
mappedDevice.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => { | ||
if (ok) { | ||
logger.info(`Succesfully configured ${friendlyName} ${ieeeAddr}`); | ||
} else { | ||
logger.error(`Failed to configure ${friendlyName} ${ieeeAddr}`); | ||
} | ||
}); | ||
|
||
// Setup an OnAfIncomingMsg handler if needed. | ||
if (mappedDevice.onAfIncomingMsg) { | ||
mappedDevice.onAfIncomingMsg.forEach((ep) => this.zigbee.registerOnAfIncomingMsg(ieeeAddr, ep)); | ||
} | ||
|
||
// Mark as configured | ||
this.configured.push(ieeeAddr); | ||
} | ||
} | ||
} | ||
|
||
module.exports = DeviceConfigure; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
const settings = require('../util/settings'); | ||
const logger = require('../util/logger'); | ||
|
||
const dontCacheProperties = ['click', 'action', 'button', 'button_left', 'button_right']; | ||
|
||
/** | ||
* This extensions handles messages received from devices. | ||
*/ | ||
class DeviceReceive { | ||
constructor(zigbee, mqtt, state, publishDeviceState) { | ||
this.zigbee = zigbee; | ||
this.mqtt = mqtt; | ||
this.state = state; | ||
this.publishDeviceState = publishDeviceState; | ||
} | ||
|
||
onZigbeeMessage(message, device, mappedDevice) { | ||
if (message.type == 'devInterview' && !settings.getDevice(message.data)) { | ||
logger.info('Connecting with device...'); | ||
this.mqtt.log('pairing', 'connecting with device'); | ||
} | ||
|
||
if (message.type == 'devIncoming') { | ||
logger.info('Device incoming...'); | ||
this.mqtt.log('pairing', 'device incoming'); | ||
} | ||
|
||
if (!device) { | ||
logger.warn('Message without device!'); | ||
return; | ||
} | ||
|
||
// Check if this is a new device. | ||
if (!settings.getDevice(device.ieeeAddr)) { | ||
logger.info(`New device with address ${device.ieeeAddr} connected!`); | ||
settings.addDevice(device.ieeeAddr); | ||
this.mqtt.log('device_connected', device.ieeeAddr); | ||
} | ||
|
||
if (!mappedDevice) { | ||
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; | ||
} | ||
|
||
// After this point we cant handle message withoud cid or cmdId anymore. | ||
if (!message.data || (!message.data.cid && !message.data.cmdId)) { | ||
return; | ||
} | ||
|
||
// Find a conveter for this message. | ||
const cid = message.data.cid; | ||
const cmdId = message.data.cmdId; | ||
const converters = mappedDevice.fromZigbee.filter((c) => { | ||
if (cid) { | ||
return c.cid === cid && c.type === message.type; | ||
} else if (cmdId) { | ||
return c.cmd === cmdId; | ||
} | ||
|
||
return false; | ||
}); | ||
|
||
// Check if there is an available converter | ||
if (!converters.length) { | ||
if (cid) { | ||
logger.warn( | ||
`No converter available for '${mappedDevice.model}' with cid '${cid}', ` + | ||
`type '${message.type}' and data '${JSON.stringify(message.data)}'` | ||
); | ||
} else if (cmdId) { | ||
logger.warn( | ||
`No converter available for '${mappedDevice.model}' with cmd '${cmdId}' ` + | ||
`and data '${JSON.stringify(message.data)}'` | ||
); | ||
} | ||
|
||
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices.`); | ||
return; | ||
} | ||
|
||
// Convert this Zigbee message to a MQTT message. | ||
// Get payload for the message. | ||
// - If a payload is returned publish it to the MQTT broker | ||
// - If NO payload is returned do nothing. This is for non-standard behaviour | ||
// for e.g. click switches where we need to count number of clicks and detect long presses. | ||
converters.forEach((converter) => { | ||
const publish = (payload) => { | ||
// Don't cache messages with following properties: | ||
let cache = true; | ||
dontCacheProperties.forEach((property) => { | ||
if (payload.hasOwnProperty(property)) { | ||
cache = false; | ||
} | ||
}); | ||
|
||
// Add device linkquality. | ||
if (message.hasOwnProperty('linkquality')) { | ||
payload.linkquality = message.linkquality; | ||
} | ||
|
||
this.publishDeviceState(device, payload, cache); | ||
}; | ||
|
||
const payload = converter.convert(mappedDevice, message, publish, settings.getDevice(device.ieeeAddr)); | ||
|
||
if (payload) { | ||
publish(payload); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
module.exports = DeviceReceive; |
Oops, something went wrong.