UNPKG

@hoobs/hue

Version:

HOOBS plugin for Philips Hue and deCONZ

440 lines (417 loc) 12.4 kB
// homebridge-hue/lib/HuePlatform.js // Copyright © 2016-2020 Erik Baauw. All rights reserved. // // Homebridge plugin for Philips Hue and/or deCONZ. // // HuePlatform provides the platform for support Philips Hue bridges and // connected devices. The platform provides discovery of bridges and setting // up a heartbeat to poll the bridges. // // Todo: // - Dynamic homebridge accessories. // - Store user (bridge password) in context of homebridge accessory for bridge. 'use strict' const fs = require('fs').promises const semver = require('semver') const zlib = require('zlib') const homebridgeLib = require('homebridge-lib') const HueBridgeModule = require('./HueBridge') const HueBridge = HueBridgeModule.HueBridge const HueDiscovery = require('../lib/HueDiscovery') const packageJson = require('../package.json') module.exports = HuePlatform function toIntBetween (value, minValue, maxValue, defaultValue) { const n = Number(value) if (isNaN(n) || n !== Math.floor(n) || n < minValue || n > maxValue) { return defaultValue } return n } function minVersion (range) { let s = range.split(' ')[0] while (s) { if (semver.valid(s)) { break } s = s.substring(1) } return s || undefined } async function gzip (data) { return new Promise((resolve, reject) => { zlib.gzip(data, (error, result) => { if (error) { return reject(error) } resolve(result) }) }) } const formatError = homebridgeLib.CommandLineTool.formatError // ===== HuePlatform =========================================================== function HuePlatform (log, configJson, homebridge) { this.log = log this.api = homebridge this.packageJson = packageJson this.configJson = configJson const my = new homebridgeLib.MyHomeKitTypes(homebridge) const eve = new homebridgeLib.EveHomeKitTypes(homebridge) HueBridgeModule.setHomebridge(homebridge, my, eve) this.config = { anyOn: true, excludeSensorTypes: {}, forceCt: true, forceHttp: false, groups: false, group0: false, heartrate: 5, hosts: [], lights: false, lowBattery: 25, nativeHomeKitLights: true, nativeHomeKitSensors: true, nupnp: true, resetTimeout: 500, resource: true, rooms: false, rules: false, schedules: false, sensors: false, timeout: 5, users: {}, waitTimeResend: 300, waitTimeUpdate: 20, wallSwitch: false } for (const key in configJson) { const value = configJson[key] switch (key.toLowerCase()) { case 'anyon': this.config.anyOn = !!value break case 'excludesensortypes': if (Array.isArray(value)) { for (const type of value) { this.config.excludeSensorTypes[type] = true switch (type) { case 'ZLLPresence': this.config.excludeSensorTypes.ZHAPresence = true break case 'ZLLLightLevel': this.config.excludeSensorTypes.ZHALightLevel = true break case 'ZLLTemperature': this.config.excludeSensorTypes.ZHATemperature = true break case 'ZLLRelativeRotary': this.config.excludeSensorTypes.ZHARelativeRotary = true break case 'ZLLSwitch': this.config.excludeSensorTypes.ZHASwitch = true break default: break } } } else { this.log.debug( 'config.json: %s: warning: ignoring non-array value', key ) } break case 'forcect': this.config.forceCt = !!value break case 'forcehttp': this.config.forceHttp = !!value break case 'forceeveweather': this.config.forceEveWeather = !!value break case 'groups': this.config.groups = !!value break case 'group0': this.config.group0 = !!value break case 'heartrate': this.config.heartrate = toIntBetween( value, 1, 30, this.config.heartrate ) break case 'host': case 'hosts': if (typeof value === 'string') { if (value !== '') { this.config.hosts.push(value) } } else if (Array.isArray(value)) { for (const host of value) { if (typeof host === 'string' && host !== '') { this.config.hosts.push(host) } } } else { this.log.debug( 'config.json: warning: %s: ignoring non-array, non-string value %j', key, value ) } break case 'huedimmerrepeat': this.config.hueDimmerRepeat = !!value break case 'huemotiontemperaturehistory': this.config.hueMotionTemperatureHistory = !!value break case 'lights': this.config.lights = !!value break case 'linkbutton': this.config.linkbutton = !!value break case 'lowbattery': this.config.lowBattery = toIntBetween( value, 0, 100, this.config.lowBattery ) break case 'name': this.name = value break case 'nativehomekitlights': this.config.nativeHomeKitLights = !!value break case 'nativehomekitsensors': this.config.nativeHomeKitSensors = !!value break case 'nupnp': this.config.nupnp = !!value break case 'parallelrequests': this.config.parallelRequests = toIntBetween( value, 1, 30, this.config.parallelRequests ) break case 'platform': break case 'resettimeout': this.config.resetTimeout = toIntBetween( value, 10, 2000, this.config.resetTimeout ) break case 'resource': this.config.resource = !!value break case 'rooms': this.config.rooms = !!value break case 'rules': this.config.rules = !!value break case 'scenes': this.config.scenes = !!value break case 'scenesasswitch': this.config.scenesAsSwitch = !!value break case 'schedules': this.config.schedules = !!value break case 'sensors': this.config.sensors = !!value break case 'timeout': this.config.timeout = toIntBetween( value, 5, 30, this.config.timeout ) break case 'users': this.config.users = value break case 'waittimeresend': this.config.waitTimeResend = toIntBetween( value, 100, 1000, this.config.waitTimeResend ) break case 'waittimeupdate': this.config.waitTimeUpdate = toIntBetween( value, 0, 500, this.config.waitTimeUpdate ) break case 'wallswitch': this.config.wallSwitch = !!value break default: this.log.debug('config.json: warning: %s: ignoring unknown key', key) break } } this.hueDiscovery = new HueDiscovery({ forceHttp: this.config.forceHttp, timeout: this.config.timeout, nupnp: this.config.nupnp }) this.bridgeMap = {} this.bridges = [] this.identify() } HuePlatform.prototype.accessories = async function (callback) { const accessoryList = [] const usernames = [] for (const bridgeId in this.configJson.users) { usernames.push(this.configJson.users[bridgeId]) } this.log.debug( 'config.json: %s', this.maskUsernames(usernames, this.maskConfigJson(this.configJson)) ) try { const jobs = [] for (const host of await this.findBridges()) { const bridge = new HueBridge(this, host) this.bridges.push(bridge) jobs.push(bridge.accessories()) } const accessoriesList = await Promise.all(jobs) for (const accessories of accessoriesList) { for (const accessory of accessories) { accessoryList.push(accessory) } } await this.dump() } catch (error) { this.log.error(formatError(error)) } if (accessoryList.length > 0) { // Setup heartbeat. let beat = -1 setInterval(() => { beat += 1 beat %= 7 * 24 * 3600 for (const bridge of this.bridges) { bridge.heartbeat(beat) } }, 1000) } try { callback(accessoryList) } catch (error) { this.log.error('homebridge: error: %s', formatError(error)) } } // ===== Troubleshooting ======================================================= HuePlatform.prototype.identify = function () { this.log.info( '%s v%s, node %s, homebridge v%s', 'homebridge-hue', packageJson.version, process.version, this.api.serverVersion ) if (semver.clean(process.version) !== minVersion(packageJson.engines.node)) { this.log.debug( 'warning: not using recommended node version v%s LTS', minVersion(packageJson.engines.node) ) } } HuePlatform.prototype.dump = async function () { const usernames = [] const obj = { versions: { node: process.version, homebridge: 'v' + this.api.serverVersion }, config: this.maskConfigJson(this.configJson), bridges: [] } for (const bridgeId in this.configJson.users) { usernames.push(this.configJson.users[bridgeId]) } obj.versions['homebridge-hue'] = 'v' + packageJson.version for (const bridge of this.bridges) { const state = bridge.fullState if (state !== undefined && state.config !== undefined) { state.config.ipaddress = this.maskHost(state.config.ipaddress) state.config.gateway = this.maskHost(state.config.gateway) if (state.config.proxyaddress !== 'none') { state.config.proxyaddress = this.maskHost(state.config.proxyaddress) } for (const username in state.config.whitelist) { usernames.push(username) } } obj.bridges.push(state) } const filename = this.api.user.storagePath() + '/homebridge-hue.json.gz' try { const data = await gzip(this.maskUsernames(usernames, obj)) await fs.writeFile(filename, data) this.log.info('masked debug info dumped to %s', filename) } catch (error) { this.log.error('error: %s: %s', filename, formatError(error)) } } HuePlatform.prototype.maskHost = function (host = '') { const elt = host.split('.') return elt.length === 4 && host !== '127.0.0.1' ? [elt[0], '***', '***', elt[3]].join('.') : host } HuePlatform.prototype.maskConfigJson = function (configJson) { const json = {} Object.assign(json, configJson) if (typeof configJson.host === 'string') { json.host = this.maskHost(configJson.host) } else if (Array.isArray(configJson.host)) { for (const id in configJson.host) { json.host[id] = this.maskHost(configJson.host[id]) } } if (typeof configJson.hosts === 'string') { json.hosts = this.maskHost(configJson.hosts) } else if (Array.isArray(configJson.hosts)) { for (const id in configJson.hosts) { json.hosts[id] = this.maskHost(configJson.hosts[id]) } } return json } HuePlatform.prototype.maskUsernames = function (usernames, json) { let s = JSON.stringify(json) let i = 0 for (const username of usernames) { i += 1 const regexp = RegExp(username, 'g') let mask = username.replace(/./g, '*') mask = (mask + i).slice(-username.length) s = s.replace(regexp, mask) } return s } // ===== Bridge Discovery ====================================================== // Return promise to list of ipaddresses of found Hue bridges. HuePlatform.prototype.findBridges = async function () { if (this.config.hosts.length > 0) { const list = [] for (const host of this.config.hosts) { list.push(host) } return list } try { this.log.info('searching bridges and gateways') const map = await this.hueDiscovery.discover() const hosts = [] for (const host in map) { this.log.debug( 'found bridge %s at %s', map[host].toUpperCase(), this.maskHost(host) ) hosts.push(host) } if (hosts.length > 0) { return hosts } this.log.info('no bridges or gateways found - retrying in 30 seconds') await homebridgeLib.timeout(30000) return this.findBridges() } catch (error) { this.log.error(formatError(error)) } }