UNPKG

homebridge-zigbee-v2

Version:

ZigBee Platform plugin for HomeBridge

329 lines (288 loc) 9.97 kB
const path = require('path') const get = require('lodash.get') const retry = require('async-retry') const requireDir = require('require-dir') const zigbee = require('./lib/zigbee') const sleep = require('./lib/utils/sleep') const castArray = require('./lib/utils/castArray') const parseModel = require('./lib/utils/parseModel') const routerPolling = require('./lib/utils/routerPolling') const findSerialPort = require('./lib/utils/findSerialPort') const addEveTypes = require('./lib/types/EveTypes') const addSetupTypes = require('./lib/types/SetupTypes') const PermitJoinAccessory = require('./lib/PermitJoinAccessory') const PLUGIN_NAME = 'homebridge-zigbee' const PLATFORM_NAME = 'ZigBeePlatform' const devices = Object.values(requireDir('./lib/devices')) // Only for beta period process.on('uncaughtException', (error) => { console.error('Uncaught Exception', error) // eslint-disable-line no-console }) process.on('unhandledRejection', (reason) => { console.error('Unhandled Rejection', reason) // eslint-disable-line no-console }) // eslint-disable-next-line one-var, one-var-declaration-per-line let Accessory, Service, Characteristic, UUIDGen module.exports = function main(homebridge) { addEveTypes(homebridge) addSetupTypes(homebridge) Accessory = homebridge.platformAccessory Service = homebridge.hap.Service Characteristic = homebridge.hap.Characteristic UUIDGen = homebridge.hap.uuid homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, ZigBeePlatform, true) } class ZigBeePlatform { constructor(log, config, api) { this.log = log this.api = api this.config = config this.devices = {} this.accessories = {} this.permitJoinAccessory = null // Bind handlers this.handleZigBeeError = this.handleZigBeeError.bind(this) this.handleZigBeeReady = this.handleZigBeeReady.bind(this) this.handleZigBeeIndication = this.handleZigBeeIndication.bind(this) this.handleInitialization = this.handleInitialization.bind(this) this.configureAccessory = this.configureAccessory.bind(this) // Listen events this.api.on('didFinishLaunching', this.handleInitialization) this.log('ZigBee platform initialization') } handleInitialization() { this.startZigBee().catch(this.log) } async startZigBee() { zigbee.init({ port: this.config.port || await findSerialPort(), db: this.config.database || path.join(this.api.user.storagePath(), './zigbee.db'), panId: this.config.panId || 0xFFFF, channel: this.config.channel || 11, }) zigbee.on('ready', this.handleZigBeeReady) zigbee.on('error', this.handleZigBeeError) zigbee.on('ind', this.handleZigBeeIndication) const retrier = async () => { try { await zigbee.start() } catch (error) { await zigbee.stop() throw error } } try { await retry(retrier, { retries: 20, minTimeout: 5000, maxTimeout: 5000, onRetry: () => this.log('Retrying connect to hardware'), }) } catch (error) { this.log('error:', error) } } handleZigBeeError(error) { this.log('error:', error) } handleZigBeeIndication(message) { // eslint-disable-line consistent-return switch (message.type) { // Supported indication messages case 'attReport': case 'statusChange': return this.handleZigBeeAttrChange(message) case 'devInterview': return this.handleZigBeeDevInterview(message) case 'devIncoming': return this.handleZigBeeDevIncoming(message) case 'devLeaving': return this.handleZigBeeDevLeaving(message) default: // Do nothing } } handleZigBeeAttrChange(message) { const ieeeAddr = get(message, 'endpoints[0].device.ieeeAddr') if (!ieeeAddr) { return this.log('Unable to parse device ieeeAddr from message:', message) } const device = this.getDevice(ieeeAddr) if (!device) { return this.log('Received message from unknown device:', ieeeAddr) } device.zigbee.handleIndicationMessage(message) } handleZigBeeDevInterview(message) { const endpoint = get(message, 'status.endpoint.current') const endpointTotal = get(message, 'status.endpoint.total') const cluster = get(message, 'status.endpoint.cluster.current') const clusterTotal = get(message, 'status.endpoint.cluster.total') this.log( `Join progress: interview endpoint ${endpoint} of ${endpointTotal} ` + `and cluster ${cluster} of ${clusterTotal}` ) } async handleZigBeeDevIncoming(message) { const ieeeAddr = message.data // Stop permit join this.permitJoinAccessory.setPermitJoin(false) this.log(`Device announced incoming and is added, id: ${ieeeAddr}`) // Ignore if the device exists if (!this.getDevice(ieeeAddr)) { // Wait a little bit for a database sync await sleep(1500) const data = zigbee.device(ieeeAddr) this.initDevice(data) } } handleZigBeeDevLeaving(message) { const ieeeAddr = message.data // Stop permit join this.permitJoinAccessory.setPermitJoin(false) this.log(`Device announced leaving and is removed, id: ${ieeeAddr}`) const uuid = UUIDGen.generate(ieeeAddr) const accessory = this.getAccessory(uuid) // Sometimes we can unpair device which doesn't exist in HomeKit if (accessory) { this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]) this.removeDevice(ieeeAddr) this.removeAccessory(uuid) } } handleZigBeeReady() { const info = zigbee.info() this.log('ZigBee platform initialized, info:') this.log('------------------------------------') this.log('channel:', info.net.channel) this.log('pan id:', info.net.panId) this.log('extended pan id:', info.net.extPanId) this.log('ieee address:', info.net.ieeeAddr) this.log('nwk address:', info.net.nwkAddr) this.log('firmware version:', info.firmware.version) this.log('firmware revision:', info.firmware.revision) this.log('------------------------------------') // Set led indicator zigbee.request('UTIL', 'ledControl', { ledid: 3, mode: this.config.disableLed ? 0 : 1, }).catch(() => { /* Unable to set led indicator, may be your device doesn\'t support it */ }) // Init permit join accessory this.initPermitJoinAccessory() // Init devices zigbee.list().forEach(data => this.initDevice(data)) // Init log for router polling service if (!this.config.disablePingLog) { routerPolling.log = this.log } // Some routers need polling to prevent them from sleeping. routerPolling.start(this.config.routerPollingInterval) } setDevice(device) { this.devices[device.ieeeAddr] = device } getDevice(ieeeAddr) { return this.devices[ieeeAddr] } setAccessory(accessory) { this.accessories[accessory.UUID] = accessory } getAccessory(uuid) { return this.accessories[uuid] } registerAccessory(accessory) { this.setAccessory(accessory) this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]) } recognizeDevice({ model, manufacturer }) { for (const Device of devices) { if (!Device.description) { continue // eslint-disable-line no-continue } if ( castArray(Device.description.model).includes(model) && castArray(Device.description.manufacturer).includes(manufacturer) ) { return Device } } } initDevice(data) { try { const platform = this const model = parseModel(data.modelId) const manufacturer = data.manufName const ieeeAddr = data.ieeeAddr const uuid = UUIDGen.generate(ieeeAddr) const accessory = this.getAccessory(uuid) const log = (...args) => this.log(manufacturer, model, ieeeAddr, ...args) const Device = this.recognizeDevice({ model, manufacturer }) const name = get(Device, 'description.name') if (!Device) { return this.log('Unrecognized device:', ieeeAddr, manufacturer, model) } const device = new Device({ name, model, manufacturer, ieeeAddr, accessory, platform, log, Accessory, Service, Characteristic, UUIDGen, }) this.setDevice(device) this.log('Registered device:', ieeeAddr, manufacturer, model) } catch (error) { this.log( `Unable to initialize device ${data && data.ieeeAddr}, ` + 'try to remove it and add it again.\n') this.log('Reason:', error) } } initPermitJoinAccessory() { const platform = this const uuid = UUIDGen.generate('zigbee:permit-join') const accessory = this.getAccessory(uuid) const log = (...args) => this.log('[PermitJoinAccessory]', ...args) this.permitJoinAccessory = new PermitJoinAccessory({ accessory, platform, log, Accessory, Service, Characteristic, UUIDGen, }) } configureAccessory(accessory) { this.setAccessory(accessory) } removeDevice(ieeeAddr) { const device = this.devices[ieeeAddr] if (device) { device.unregister() delete this.devices[ieeeAddr] this.removeAccessory(UUIDGen.generate(ieeeAddr)) } } removeAccessory(uuid) { delete this.accessories[uuid] } async unpairDevice(device) { try { this.log('Unpairing device:', device.ieeeAddr) await zigbee.remove(device.ieeeAddr) } catch (error) { this.log('Unable to unpairing properly, trying to unregister device:', device.ieeeAddr) await zigbee.unregister(device.ieeeAddr) } finally { this.log('Device has been unpaired:', device.ieeeAddr) this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [device.accessory]) this.removeDevice(device.ieeeAddr) } } }