UNPKG

homebridge-hue

Version:
1,198 lines (1,144 loc) 38.3 kB
// homebridge-hue/lib/HueBridge.js // Copyright © 2016-2025 Erik Baauw. All rights reserved. // // Homebridge plugin for Philips Hue. import { format } from 'node:util' import { formatError, timeout } from 'homebridge-lib' import { semver } from 'homebridge-lib/semver' import { EventStreamClient } from 'hb-hue-tools/EventStreamClient' import { HueClient } from 'hb-hue-tools/HueClient' import { HueAccessory } from './HueAccessory.js' import { HueSchedule } from './HueSchedule.js' let Service let Characteristic let my class HueBridge { static setHomebridge (homebridge, _my, _eve) { HueAccessory.setHomebridge(homebridge, _my, _eve) HueSchedule.setHomebridge(homebridge, _my) Service = homebridge.hap.Service Characteristic = homebridge.hap.Characteristic my = _my } constructor (platform, host, bridge) { this.log = platform.log this.platform = platform this.host = host this.bridge = bridge this.hostname = host.split(':')[0] this.name = this.hostname this.type = 'bridge' this.defaultTransitiontime = 0.4 this.state = { heartrate: this.platform.config.heartrate, transitiontime: this.defaultTransitiontime, bri: 1, request: 0, lights: 0, groups: 0, group0: 0, sensors: 0, schedules: 0, rules: 0 } this.serviceList = [] this.lights = {} this.groups = {} this.sensors = {} this.schedules = {} this.rules = {} this.whitelist = { lights: {}, groups: {}, scenes: {}, sensors: {}, schedules: {}, rules: {} } this.blacklist = { lights: {}, groups: {}, scenes: {}, sensors: {}, schedules: {}, rules: {} } this.multiclip = {} this.multilight = {} this.splitlight = {} this.outlet = { groups: {}, lights: {} } this.switch = { groups: {}, lights: {} } this.valve = {} this.wallswitch = {} } getServices () { this.log.info('%s: %d services', this.name, this.serviceList.length) return this.serviceList } async accessories () { this.accessoryMap = {} this.accessoryList = [] try { await this.exposeBridge() await this.createUser() const state = await this.getFullState() await this.exposeResources(state) this.platform.bridgeMap[this.bridge.bridgeid] = this } catch (error) { if (error.message !== 'unknown bridge') { this.log.warn('%s: %s - retrying in 15s', this.name, formatError(error)) await timeout(15000) return this.accessories() } } this.log.info('%s: %d accessories', this.name, this.accessoryList.length) return this.accessoryList } getInfoService () { return this.infoService } async exposeBridge () { this.name = this.bridge.name this.serialNumber = this.bridge.bridgeid this.uuid_base = this.serialNumber this.apiKey = this.platform.config.users[this.serialNumber] || '' this.config = { parallelRequests: 10, nativeHomeKitLights: this.platform.config.nativeHomeKitLights, nativeHomeKitSensors: this.platform.config.nativeHomeKitSensors } this.model = this.bridge.modelid if ( this.model === 'BSB002' && !HueClient.isHueBridge(this.bridge) ) { this.model = 'HA-Bridge' } if (this.model == null) { this.model = 'Tasmota' } this.philips = 'Philips' const recommendedVersion = this.platform.packageJson.engines[this.bridge.modelid] switch (this.model) { case 'BSB001': // Philips Hue v1 (round) bridge; this.config.parallelRequests = 3 this.config.nativeHomeKitLights = false this.config.nativeHomeKitSensors = false /* falls through */ case 'BSB002': // Philips Hue v2 (square) bridge; this.isHue = true this.version = this.bridge.apiversion if (semver.gte(this.version, '1.36.0')) { this.philips = 'Signify Netherlands B.V.' } this.manufacturer = this.philips this.idString = format( '%s: %s %s %s v%s, api v%s', this.name, this.manufacturer, this.model, this.type, this.bridge.swversion, this.bridge.apiversion ) this.log.info(this.idString) // if (this.model === 'BSB002' && this.platform.config.homebridgeHue2 === '') { // this.log.warn( // '%s: warning: support for the gen-2 Hue bridge will be deprecated in favour of Homebridge Hue2', // this.name // ) // } if (!semver.satisfies(this.version, recommendedVersion)) { this.log.warn( '%s: warning: not using recommended Hue bridge api version %s', this.name, recommendedVersion ) } this.config.link = semver.lt(this.version, '1.31.0') break case 'HA-Bridge': this.manufacturer = 'HA-Bridge' this.idString = format( '%s: %s v%s, api v%s', this.name, this.model, this.bridge.swversion, this.bridge.apiversion ) this.log.info(this.idString) this.version = this.bridge.apiversion this.config.nativeHomeKitLights = false this.config.nativeHomeKitSensors = false break case 'Tasmota': this.manufacturer = 'Sonoff' this.idString = format( '%s: %s %s v%s, api v%s', this.name, this.manufacturer, this.model, this.bridge.swversion, this.bridge.apiversion ) this.version = this.bridge.apiversion this.config.nativeHomeKitLights = false this.config.nativeHomeKitSensors = false this.apiKey = 'homebridgehue' break default: this.log.warn( '%s: warning: ignoring unknown bridge %j', this.name, this.bridge ) throw new Error('unknown bridge') } this.config.linkButton = this.platform.config.linkButton == null ? this.config.link : this.platform.config.linkButton const options = { config: this.bridge, forceHttp: this.platform.config.forceHttp, host: this.host, keepAlive: true, maxSockets: this.platform.config.parallelRequests || this.config.parallelRequests, timeout: this.platform.config.timeout, waitTimePut: this.platform.config.waitTimePut, waitTimePutGroup: this.platform.config.waitTimePutGroup, waitTimeResend: this.platform.config.waitTimeResend } if (this.apiKey !== '') { options.apiKey = this.apiKey } this.hueClient = new HueClient(options) this.hueClient .on('error', (error) => { if (error.request.id !== this.requestId) { if (error.request.body == null) { this.log( '%s: request %d: %s %s', this.name, error.request.id, error.request.method, error.request.resource ) } else { this.log( '%s: request %d: %s %s %s', this.name, error.request.id, error.request.method, error.request.resource, error.request.body ) } this.requestId = error.request.id } this.log.warn( '%s: request %d: %s', this.name, error.request.id, formatError(error) ) }) .on('request', (request) => { if (request.body == null) { this.log.debug( '%s: request %d: %s %s', this.name, request.id, request.method, request.resource ) } else { this.log.debug( '%s: request %d: %s %s %s', this.name, request.id, request.method, request.resource, request.body ) } }) .on('response', (response) => { this.log.debug( '%s: request %d: %d %s', this.name, response.request.id, response.statusCode, response.statusMessage ) }) this.infoService = new Service.AccessoryInformation() this.serviceList.push(this.infoService) this.infoService .updateCharacteristic(Characteristic.Manufacturer, this.manufacturer) .updateCharacteristic(Characteristic.Model, this.model) .updateCharacteristic(Characteristic.SerialNumber, this.serialNumber) .updateCharacteristic(Characteristic.FirmwareRevision, this.version) this.service = new my.Services.HueBridge(this.name) this.service.setPrimaryService() this.serviceList.push(this.service) this.service.getCharacteristic(my.Characteristics.Heartrate) .updateValue(this.state.heartrate) .on('set', this.setHeartrate.bind(this)) this.service.getCharacteristic(my.Characteristics.LastUpdated) .updateValue(String(new Date()).slice(0, 24)) this.service.getCharacteristic(my.Characteristics.TransitionTime) .updateValue(this.state.transitiontime) .on('set', this.setTransitionTime.bind(this)) this.service.addOptionalCharacteristic(Characteristic.Brightness) if (this.isHue) { this.service.getCharacteristic(my.Characteristics.Restart) .updateValue(false) .on('set', this.setRestart.bind(this)) } if (this.config.linkButton) { this.switchService = new Service.StatelessProgrammableSwitch(this.name) this.serviceList.push(this.switchService) this.switchService .getCharacteristic(Characteristic.ProgrammableSwitchEvent) .setProps({ minValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, maxValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS }) if (this.config.link) { this.state.linkbutton = false this.state.hkLink = false this.service.getCharacteristic(my.Characteristics.Link) .updateValue(this.state.hkLink) .on('set', this.setLink.bind(this)) } } if (this.isHue) { this.state.hkSearch = false this.service.getCharacteristic(my.Characteristics.Search) .updateValue(false) .on('set', this.setSearch.bind(this)) } this.accessoryList.push(this) } async createUser () { if (this.apiKey) { return } try { this.apiKey = await this.hueClient.getApiKey('homebridge-hue') let s = '\n' s += ' "platforms": [\n' s += ' {\n' s += ' "platform": "Hue",\n' s += ' "users": {\n' s += ' "' + this.serialNumber + '": "' + this.apiKey + '"\n' s += ' }\n' s += ' }\n' s += ' ]' this.log.info( '%s: created user - please edit config.json and restart homebridge%s', this.name, s ) } catch (error) { if (error.request != null) { if (error.type === 101) { this.log.info( '%s: press link button on the bridge to create a user - retrying in 15s', this.name ) } } else { this.log.error('%s: %s', this.name, formatError(error)) } await timeout(15000) return this.createUser() } } async getFullState () { const state = await this.get('/') if (state == null || state.groups == null) { throw new Error('cannot get full state') } try { const group0 = await this.get('/groups/0') state.groups[0] = group0 } catch (error) { this.log.warn('%s: warning: /groups/0 blacklisted', this.name) this.blacklist.groups[0] = true } if (state.resourcelinks == null) { const resourcelinks = await this.get('/resourcelinks') state.resourcelinks = resourcelinks } this.fullState = state return state } async exposeResources (obj) { this.obj = obj.config for (const key in obj.resourcelinks) { const link = obj.resourcelinks[key] if ( link.name === 'homebridge-hue' && link.links && link.description && ( !this.platform.config.ownResourcelinks || link.owner === this.hueClient.apiKey ) ) { const list = link.description.toLowerCase() switch (list) { case 'blacklist': case 'lightlist': case 'multiclip': case 'multilight': case 'outlet': case 'splitlight': case 'switch': case 'valve': case 'wallswitch': case 'whitelist': break default: this.log.warn( '%s: /resourcelinks/%d: ignoring unknown description %s', this.name, key, link.description ) continue } this.log.debug( '%s: /resourcelinks/%d: %d %s entries', this.name, key, link.links.length, list ) let accessory for (const resource of link.links) { const type = resource.split('/')[1] const id = resource.split('/')[2] if (!this.whitelist[type]) { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported resource', this.name, key, resource ) continue } if (list === 'blacklist') { this.blacklist[type][id] = true continue } if (obj[type][id] === undefined) { this.log( '%s: /resourcelinks/%d: %s: not available', this.name, key, resource ) continue } if (list === 'multiclip') { if ( type !== 'sensors' || ( obj[type][id].type.slice(0, 4) !== 'CLIP' && obj[type][id].type !== 'Daylight' ) ) { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported multiclip resource', this.name, key, resource ) continue } if (this.multiclip[id] != null) { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring duplicate multiclip resource', this.name, key, resource ) continue } this.multiclip[id] = key if (accessory == null) { // First resource const serialNumber = this.serialNumber + '-' + id accessory = new HueAccessory(this, serialNumber, true) this.accessoryMap[serialNumber] = accessory } accessory.addSensorResource(id, obj[type][id], false) } else if (list === 'multilight') { if (type !== 'lights') { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported multilight resource', this.name, key, resource ) continue } if (this.multilight[id] != null) { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring duplicate multilight resource', this.name, key, resource ) continue } this.multilight[id] = key if (accessory == null) { // First resource const a = obj[type][id].uniqueid .match(/(..:..:..:..:..:..:..:..)-..(:?-....)?/) const serialNumber = a[1].replace(/:/g, '').toUpperCase() accessory = new HueAccessory(this, serialNumber, true) this.accessoryMap[serialNumber] = accessory } accessory.addLightResource(id, obj[type][id]) } else if (list === 'outlet') { if (type !== 'groups' && type !== 'lights') { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported outlet resource', this.name, key, resource ) continue } this.outlet[type][id] = true } else if (list === 'splitlight') { if (type !== 'lights') { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported splitlight resource', this.name, key, resource ) continue } this.splitlight[id] = true } else if (list === 'switch') { if (type !== 'groups' && type !== 'lights') { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported switch resource', this.name, key, resource ) continue } this.switch[type][id] = true } else if (list === 'valve') { if (type !== 'lights') { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported valve resource', this.name, key, resource ) continue } this.valve[id] = true } else if (list === 'wallswitch') { if (type !== 'lights') { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported wallswitch resource', this.name, key, resource ) continue } this.wallswitch[id] = true } else if (list === 'whitelist') { this.whitelist[type][id] = true } } } else if ( key === this.platform.config.homebridgeHue2 && link.name === 'homebridge-hue2' && link.links && link.description === 'migration' ) { this.log.debug( '%s: /resourcelinks/%d: %d entries exposed by Homebridge Hue2', this.name, key, link.links.length ) for (const resource of link.links) { const type = resource.split('/')[1] const id = resource.split('/')[2] if (!this.whitelist[type]) { this.log.warn( '%s: /resourcelinks/%d: %s: ignoring unsupported resource', this.name, key, resource ) continue } this.blacklist[type][id] = true } } } this.log.debug( '%s: %s: %s %s %s "%s"', this.name, this.serialNumber, this.manufacturer, this.model, this.type, this.name ) if (this.isHue) { for (const id in obj.groups) { obj.groups[id].scenes = [] } for (const key in obj.scenes) { if (this.platform.config.scenes && this.blacklist.scenes[key]) { this.log.debug('%s: /scenes/%s: blacklisted', this.name, key) } else if (this.platform.config.scenes || this.whitelist.scenes[key]) { const scene = obj.scenes[key] const id = scene.group == null ? 0 : scene.group this.log.debug('%s: /scenes/%s: group: %d', this.name, key, id) obj.groups[id].scenes.push({ id: key, name: scene.name }) } } } this.exposeGroups(obj.groups) this.exposeLights(obj.lights) this.exposeSensors(obj.sensors) this.exposeSchedules(obj.schedules) this.exposeRules(obj.rules) for (const id in this.accessoryMap) { const accessoryList = this.accessoryMap[id].expose() for (const accessory of accessoryList) { this.accessoryList.push(accessory) } } this.state.sensors = Object.keys(this.sensors).length this.log.debug('%s: %d sensors', this.name, this.state.sensors) this.state.lights = Object.keys(this.lights).length this.log.debug('%s: %d lights', this.name, this.state.lights) this.state.groups = Object.keys(this.groups).length this.state.group0 = this.groups[0] !== undefined ? 1 : 0 this.state.schedules = Object.keys(this.schedules).length this.log.debug('%s: %d schedules', this.name, this.state.schedules) this.state.rules = Object.keys(this.rules).length this.log.debug('%s: %d rules', this.name, this.state.rules) this.log.debug('%s: %d groups', this.name, this.state.groups) if (this.hueClient.isHue2 && !this.platform.config.forceHttp) { await this.listen() } } sensorSerialNumber (id, obj) { let serialNumber = this.serialNumber + '-' + id if (obj.type[0] === 'Z') { const uniqueid = obj.uniqueid == null ? '' : obj.uniqueid const a = uniqueid.match(/(..:..:..:..:..:..:..:..)-..(:?-....)?/) if (a != null) { // ZigBee sensor serialNumber = a[1].replace(/:/g, '').toUpperCase() if (this.platform.config.hueMotionTemperatureHistory) { // Separate accessory for Hue motion sensor's temperature. if ( ['Philips', 'Signify Netherlands B.V.'].includes(obj.manufacturername) && ['SML001', 'SML002', 'SML003', 'SML004'].includes(obj.modelid) ) { // Hue motion sensor. if (obj.type === 'ZLLTemperature') { serialNumber += '-T' } } } } } return serialNumber } isLightSensor (id, obj) { const serialNumber = this.sensorSerialNumber(id, obj) // FIXME: accessory null when lights aren't exposed. const accessory = this.accessoryMap[serialNumber] return accessory != null && accessory.resources.lights.other.length > 0 } exposeSensors (sensors) { for (const id in sensors) { const sensor = sensors[id] if (this.whitelist.sensors[id]) { this.exposeSensor(id, sensor) } else if ( this.platform.config.sensors || ( this.platform.config.lightSensors && this.isLightSensor(id, sensor) ) ) { if (this.blacklist.sensors[id]) { this.log.debug('%s: /sensors/%d: blacklisted', this.name, id) } else if (this.multiclip[id] != null) { // already exposed } else if ( this.config.nativeHomeKitSensors && sensor.type[0] === 'Z' && ( sensor.manufacturername === this.philips || sensor.manufacturername === 'PhilipsFoH' ) ) { this.log.debug('%s: /sensors/%d: exposed by bridge', this.name, id) } else if ( this.platform.config.excludeSensorTypes[sensor.type] || ( sensor.type.slice(0, 4) === 'CLIP' && this.platform.config.excludeSensorTypes.CLIP ) ) { this.log.debug( '%s: /sensors/%d: %s excluded', this.name, id, sensor.type ) } else if ( this.platform.config.excludeLightSensors && this.isLightSensor(id, sensor) ) { this.log.debug( '%s: /sensors/%d: light sensors excluded', this.name, id ) } else if ( sensor.name === '_dummy' || sensor.uniqueid === '_dummy' ) { this.log.debug( '%s: /sensors/%d: ignoring dummy sensor', this.name, id ) } else { this.exposeSensor(id, sensor) } } } } exposeSensor (id, obj) { const serialNumber = this.sensorSerialNumber(id, obj) obj.manufacturername = obj.manufacturername.replace(/\//g, '') if ( obj.manufacturername === 'homebridge-hue' && obj.modelid === obj.type && obj.uniqueid.split('-')[1] === id ) { // Combine multiple CLIP sensors into one accessory. this.log.warn( '%s: /sensors/%d: error: old multiCLIP setup has been deprecated', this.name, id ) } let accessory = this.accessoryMap[serialNumber] if (accessory == null) { accessory = new HueAccessory(this, serialNumber) this.accessoryMap[serialNumber] = accessory } accessory.addSensorResource(id, obj) } exposeLights (lights) { for (const id in lights) { const light = lights[id] if (this.whitelist.lights[id]) { this.exposeLight(id, light) } else if (this.platform.config.lights) { if (this.blacklist.lights[id]) { this.log.debug('%s: /lights/%d: blacklisted', this.name, id) } else if (this.multilight[id]) { // Already exposed. } else if ( this.config.nativeHomeKitLights && ( (light.capabilities != null && light.capabilities.certified) || (light.capabilities == null && light.manufacturername === this.philips) ) ) { this.log.debug('%s: /lights/%d: exposed by bridge %j', this.name, id, light) } else { this.exposeLight(id, light) } } } } exposeLight (id, obj) { if (obj.manufacturername != null) { obj.manufacturername = obj.manufacturername.replace(/\//g, '') } let serialNumber = this.serialNumber + '-L' + id const uniqueid = obj.uniqueid == null ? '' : obj.uniqueid const a = uniqueid.match(/(..:..:..:..:..:..:..:..)-(..)(:?-....)?/) if (a != null && this.model !== 'HA-Bridge') { serialNumber = a[1].replace(/:/g, '').toUpperCase() if (this.splitlight[id]) { serialNumber += '-' + a[2].toUpperCase() } } let accessory = this.accessoryMap[serialNumber] if (accessory == null) { accessory = new HueAccessory(this, serialNumber) this.accessoryMap[serialNumber] = accessory } accessory.addLightResource(id, obj) } exposeGroups (groups) { for (const id in groups) { const group = groups[id] if (this.whitelist.groups[id]) { this.exposeGroup(id, group) } else if (this.platform.config.groups) { if (this.blacklist.groups[id]) { this.log.debug('%s: /groups/%d: blacklisted', this.name, id) } else if (group.type === 'Room' && !this.platform.config.rooms) { this.log.debug( '%s: /groups/%d: %s excluded', this.name, id, group.type ) } else if (id === '0' && !this.platform.config.group0) { this.log.debug('%s: /groups/%d: group 0 excluded', this.name, id) } else { this.exposeGroup(id, group) } } } } exposeGroup (id, obj) { const serialNumber = this.serialNumber + '-G' + id let accessory = this.accessoryMap[serialNumber] if (accessory == null) { accessory = new HueAccessory(this, serialNumber) this.accessoryMap[serialNumber] = accessory } accessory.addGroupResource(id, obj) } exposeSchedules (schedules) { for (const id in schedules) { if (this.whitelist.schedules[id]) { this.exposeSchedule(id, schedules[id]) } else if (this.platform.config.schedules) { if (this.blacklist.schedules[id]) { this.log.debug('%s: /schedules/%d: blacklisted', this.name, id) } else { this.exposeSchedule(id, schedules[id]) } } } } exposeSchedule (id, obj) { this.log.debug( '%s: /schedules/%d: "%s"', this.name, id, obj.name ) try { this.schedules[id] = new HueSchedule(this, id, obj) // this.accessoryList.push(this.schedules[id]); if (this.serviceList.length < 99) { this.serviceList.push(this.schedules[id].service) } } catch (e) { this.log.error( '%s: error: /schedules/%d: %j\n%s', this.name, id, obj, formatError(e) ) } } exposeRules (rules) { for (const id in rules) { if (this.whitelist.rules[id]) { this.log.debug('%s: /rules/%d: whitelisted', this.name, id) } else if (this.platform.config.rules) { if (this.blacklist.rules[id]) { this.log.debug('%s: /rules/%d: blacklisted', this.name, id) } else { this.exposeRule(id, rules[id]) } } } } exposeRule (id, obj) { this.log.debug('%s: /rules/%d: "%s"', this.name, id, obj.name) try { this.rules[id] = new HueSchedule(this, id, obj, 'rule') // this.accessoryList.push(this.rules[id]); if (this.serviceList.length < 99) { this.serviceList.push(this.rules[id].service) } } catch (e) { this.log.error( '%s: error: /rules/%d: %j\n%s', this.name, id, obj, formatError(e) ) } } resetTransitionTime () { if (this.state.resetTimer) { return } this.state.resetTimer = setTimeout(() => { this.log.info( '%s: reset homekit transition time from %ss to %ss', this.name, this.state.transitiontime, this.defaultTransitiontime ) this.state.transitiontime = this.defaultTransitiontime this.service.getCharacteristic(my.Characteristics.TransitionTime) .updateValue(this.state.transitiontime) delete this.state.resetTimer }, this.platform.config.waitTimeUpdate) } // ===== Event Stream ========================================================== async listen () { this.eventStream = new EventStreamClient(this.hueClient, { retryTime: 15 }) this.eventStream .on('error', (error) => { if (error.request == null) { this.log.warn('%s: event stream error: %s', this.name, formatError(error)) return } this.log( '%s: event stream request %d: %s %s', this.name, error.request.id, error.request.method, error.request.resource ) this.log.warn( '%s: event stream request %d: %s', this.name, error.request.id, formatError(error) ) }) .on('request', (request) => { if (request.body == null) { this.log.debug( '%s: event stream request %d: %s %s', this.name, request.id, request.method, request.resource ) } else { this.log.debug( '%s: event stream request %d: %s %s %s', this.name, request.id, request.method, request.resource, request.body ) } }) .on('response', (response) => { this.log.debug( '%s: event stream request %d: %d %s', this.name, response.request.id, response.statusCode, response.statusMessage ) }) .on('listening', (url) => { this.log('%s: event stream connected to %s', this.name, url) }) .on('closed', (url) => { this.log.warn( '%s: event stream connection to %s closed - retrying in 15s', this.name, url ) }) .on('notification', (body) => { this.log.debug('%s: event: %j', this.name, body) }) .on('changed', (resource, body) => { try { const r = resource.split('/') if (r[1] === 'scenes') { this.log.debug('%s: changed event: %j', resource, body) return } const a = this[r[1]][r[2]] if (a) { if (r[3] === 'state') { this.log.debug('%s: state changed event: %j', a.name, body) a.checkState(body, true) } else if (r[3] === 'config') { this.log.debug('%s: config changed event: %j', a.name, body) a.checkConfig(body, true) } } } catch (error) { this.log.warn('%s: event stream error: %s', this.name, formatError(error)) } }) await this.eventStream.init() this.eventStream.listen() } // ===== Heartbeat ============================================================= async heartbeat (beat) { if (beat % this.state.heartrate === 0) { this.service.getCharacteristic(my.Characteristics.LastUpdated) .updateValue(String(new Date()).slice(0, 24)) try { await this.heartbeatConfig(beat) await this.heartbeatSensors(beat) await this.heartbeatLights(beat) await this.heartbeatGroup0(beat) await this.heartbeatGroups(beat) await this.heartbeatSchedules(beat) await this.heartbeatRules(beat) } catch (error) { if (error.request == null) { this.log.warn('%s: heartbeat error: %s', this.name, formatError(error)) } } } if (beat % 600 === 0) { try { for (const id in this.sensors) { this.sensors[id].addEntry() } } catch (error) { this.log.warn('%s: heartbeat error: %s', this.name, formatError(error)) } } } async heartbeatSensors (beat) { if (this.state.sensors === 0) { return } const sensors = await this.get('/sensors') for (const id in sensors) { const a = this.sensors[id] if (a) { a.heartbeat(beat, sensors[id]) } } } async heartbeatConfig (beat) { if (!this.config.link) { return } const config = await this.get('/config') if (config.linkbutton !== this.state.linkbutton) { this.log.debug( '%s: %s linkbutton changed from %s to %s', this.name, this.type, this.state.linkbutton, config.linkbutton ) this.state.linkbutton = config.linkbutton if (this.state.linkbutton) { this.log( '%s: homekit linkbutton single press', this.switchService.displayName ) this.switchService.updateCharacteristic( Characteristic.ProgrammableSwitchEvent, Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS ) await this.put('/config', { linkbutton: false }) this.state.linkbutton = false } else { const hkLink = false if (hkLink !== this.state.hkLink) { this.log( '%s: set homekit link from %s to %s', this.name, this.state.hkLink, hkLink ) this.state.hkLink = hkLink this.service .updateCharacteristic(my.Characteristics.Link, this.state.hkLink) } } } } async heartbeatLights (beat) { if (this.state.lights === 0) { return } const lights = await this.get('/lights') for (const id in lights) { const a = this.lights[id] if (a) { a.heartbeat(beat, lights[id]) } } } async heartbeatGroups (beat) { if (this.state.groups - this.state.group0 === 0) { return } const groups = await this.get('/groups') for (const id in groups) { const a = this.groups[id] if (a) { a.heartbeat(beat, groups[id]) } } } async heartbeatGroup0 (beat) { if (this.state.group0 === 0) { return } const group0 = await this.get('/groups/0') const a = this.groups[0] if (a) { a.heartbeat(beat, group0) } } async heartbeatSchedules (beat) { if (this.state.schedules === 0) { return } const schedules = await this.get('/schedules') for (const id in schedules) { const a = this.schedules[id] if (a) { a.heartbeat(beat, schedules[id]) } } } async heartbeatRules (beat) { if (this.state.rules === 0) { return } const rules = await this.get('/rules') for (const id in rules) { const a = this.rules[id] if (a) { a.heartbeat(beat, rules[id]) } } } // ===== Homekit Events ======================================================== setHeartrate (rate, callback) { rate = Math.round(rate) if (rate === this.state.heartrate) { return callback() } this.log.info( '%s: homekit heartrate changed from %ss to %ss', this.name, this.state.heartrate, rate ) this.state.heartrate = rate return callback() } setLink (link, callback) { if (link === this.state.hkLink) { return callback() } this.log.info( '%s: homekit link changed from %s to %s', this.name, this.state.hkLink, link ) this.state.hkLink = link const newValue = link this.put('/config', { linkbutton: newValue }).then(() => { this.state.linkbutton = newValue return callback() }).catch((error) => { return callback(error) }) } setSearch (search, callback) { if (search === this.state.hkSearch) { return callback() } if (this.searchTimer != null) { clearTimeout(this.searchTimer) } this.log.info( '%s: homekit search changed from %s to %s', this.name, this.state.hkSearch, search ) if (search) { this.post('/lights').then(() => { this.searchTimer = setTimeout(() => { delete this.searchTimer this.log( '%s: set homekit search from %s to false', this.name, this.state.hkSearch ) this.state.hkSearch = false this.service .updateCharacteristic(my.Characteristics.Search, this.state.hkSearch) }, 60 * 1000) return callback() }).catch((error) => { return callback(error) }) } } setTransitionTime (transitiontime, callback) { transitiontime = Math.round(transitiontime * 10) / 10 if (transitiontime === this.state.transitiontime) { return callback() } this.log.info( '%s: homekit transition time changed from %ss to %ss', this.name, this.state.transitiontime, transitiontime ) this.state.transitiontime = transitiontime return callback() } setRestart (restart, callback) { if (!restart) { return callback() } this.log.info('%s: restart', this.name) this.hueClient.restart().then((obj) => { setTimeout(() => { this.service.setCharacteristic(my.Characteristics.Restart, false) }, this.platform.config.resetTimeout) return callback() }).catch((error) => { return callback(error) }) } identify (callback) { this.log.info('%s: identify', this.name) this.platform.identify() this.log.info(this.idString) callback() } async get (resource) { try { return this.hueClient.get(resource) } catch (error) { if (error.request == null) { this.log.error('%s: %s', this.name, formatError(error)) } throw error } } async post (resource, body) { try { return this.hueClient.post(resource, body) } catch (error) { if (error.request == null) { this.log.error('%s: %s', this.name, formatError(error)) } throw error } } async put (resource, body) { try { return this.hueClient.put(resource, body) } catch (error) { if (error.request == null) { this.log.error('%s: %s', this.name, formatError(error)) } throw error } } } export { HueBridge }