UNPKG

homebridge-hue

Version:
1,580 lines (1,527 loc) 49.5 kB
// homebridge-hue/lib/HueLight.js // Copyright © 2016-2026 Erik Baauw. All rights reserved. // // Homebridge plugin for Philips Hue. import { formatError } from 'homebridge-lib' /* eslint-disable no-unused-vars */ import { Platform } from 'homebridge-lib/Platform' import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate' /* eslint-ensable no-unused-vars */ import { AdaptiveLighting } from 'homebridge-lib/AdaptiveLighting' import { Colour } from 'homebridge-lib/Colour' const { defaultGamut, xyToHsv, hsvToXy, ctToXy } = Colour const knownLights = { '3A Smart Home DE': { // Issue #883. }, 'Busch-Jaeger': { // See: https://www.busch-jaeger.de/en/products/product-solutions/dimmer/busch-radio-controlled-dimmer-zigbee-light-link/ models: { RM01: { // 6715 U-500 with 6736-84 fix: function () { // Issue #241 if (this.config.bri && this.obj.type === 'On/Off light') { this.log.debug( '%s: %s: ignoring state.bri for %s', this.bridge.name, this.resource, this.obj.type ) this.config.bri = false } } } } }, 'dresden elektronik': { // See: https://www.dresden-elektronik.de/funktechnik/solutions/wireless-light-control/wireless-ballasts/?L=1 computesXy: true }, FeiBit: { models: { 'FNB56-SKT1EHG1.2': { fix: function () { // issue #361 this.obj.type = 'On/Off plug-in unit' } } } }, 'Feibit Inc co.': { // Issue #171 }, GLEDOPTO: { // See: https://www.led-trading.de/zigbee-kompatibel-controller-led-lichtsteuerung gamut: { r: [0.7006, 0.2993], g: [0.1387, 0.8148], b: [0.1510, 0.0227] }, models: { 'GL-C-007': { fix: function () { this.config.waitTimeUpdate = 0 } }, 'GL-C-008': { fix: function () { this.config.waitTimeUpdate = 0 } }, GLEDOPTO: { // Issue #244 fix: function () { if ( this.subtype === '0a' && this.obj.type === 'Dimmable light' && this.version === '1.0.2' ) { this.model = 'RGBW' } else if ( this.subtype === '0b' && this.obj.type === 'Color temperature light' && this.version === '1.3.002' ) { this.model = 'WW/CW' } else if ( this.subtype === '0b' && this.obj.type === 'Extended color light' && this.version === '1.0.2' ) { this.model = 'RGB+CCT' if (this.accessory.resources.lights.other.length > 1) { this.model = 'RGBW' this.config.ct = false } } else { this.log.warn( '%s: %s: unknown light model %j', this.bridge.name, this.resource, this.obj ) return } this.log.debug( '%s: %s: set model to %j', this.bridge.name, this.resource, this.model ) } } } }, icasa: { }, 'IKEA of Sweden': { // See: http://www.ikea.com/us/en/catalog/categories/departments/lighting/smart_lighting/ gamut: defaultGamut, // Issue #956 noTransition: true, models: { '': { // Hue bridge chokes on non-printable character fix: function () { this.model = 'TRADFRI bulb E27 WS opal 980lm' this.log.debug( '%s: %s: set model to %j', this.bridge.name, this.resource, this.model ) } }, 'TRADFRI bulb E27 CWS 806lm': { computesXy: true, gamut: { r: [0.68, 0.31], g: [0.11, 0.82], b: [0.13, 0.04] }, fix: function () { if (this.obj.swversion === '1.0.021') { this.config.noTransition = false } } } } }, innr: { // See: https://shop.innrlighting.com/en/shop gamut: { // Issue #152 r: [0.8817, 0.1033], g: [0.2204, 0.7758], b: [0.0551, 0.1940] }, models: { 'DL 110': { noAlert: true }, // Spot 'FL 110': { noAlert: true }, // Flex Light 'PL 110': { noAlert: true }, // Puck Light 'SL 110 M': { noAlert: true }, // Spot, issue #166 'SL 110 N': { noAlert: true }, // Spot, issue #166 'SL 110 W': { noAlert: true }, // Spot, issue #166 'SP 120': { fix: function () { this.config.bri = false } }, // smart plug 'ST 110': { noAlert: true }, // Strip 'UC 110': { noAlert: true } // Under Cabinet } }, LEDVANCE: { gamut: { r: [0.6972, 0.3027], g: [0.1737, 0.6991], b: [0.1227, 0.0959] } }, MLI: { // Issue #439 gamut: { r: [0.68, 0.31], g: [0.11, 0.82], b: [0.13, 0.04] } }, 'Neuhaus Lighting Group': { // Issue #850 }, 'Neuhaus Lighting Group ': { // Issue #455 }, OSRAM: { gamut: { r: [0.6850, 0.3149], g: [0.1780, 0.7253], b: [0.1241, 0.0578] }, fix: function () { if (this.obj.swversion === 'V1.03.07') { this.config.noTransition = true } } }, 'Paulmann Licht': { // Issue #1041 }, Pee: { // Issue #217 }, ShenZhen_Homa: { // PR #234, issue #235 }, 'Signify Netherlands B.V.': { // See: http://www.developers.meethue.com/documentation/supported-lights gamuts: { // Color gamut per light model. A: { // Color Lights r: [0.7040, 0.2960], g: [0.2151, 0.7106], b: [0.1380, 0.0800] }, B: { // Extended Color Lights r: [0.6750, 0.3220], g: [0.4090, 0.5180], b: [0.1670, 0.0400] }, C: { // next gen Extended Color Lights r: [0.6920, 0.3080], g: [0.1700, 0.7000], b: [0.1530, 0.0480] } }, computesXy: true, fix: function () { this.manufacturer = 'Signify Netherlands B.V.' }, models: { LLC001: { // Living Colors Gen1 Iris fix: function () { if (this.obj.uniqueid === 'ff:ff:ff:ff:ff:ff:ff:ff-0b') { this.serialNumber = this.bridge.serialNumber + '-L' + this.id } this.config.xy = false this.config.hs = true }, noWallSwitch: true }, LCT001: { gamut: 'B' }, // Hue bulb A19 LCT002: { gamut: 'B' }, // Hue Spot BR30 LCT003: { gamut: 'B' }, // Hue Spot GU10 LCT007: { gamut: 'B' }, // Hue bulb A19 LCT010: { gamut: 'C' }, // Hue bulb A19 LCT011: { gamut: 'C' }, // Hue BR30 LCT012: { gamut: 'C' }, // Hue Color Candle LCT014: { gamut: 'C' }, // Hue bulb A19 LCT015: { gamut: 'C' }, // Hue bulb A19 LCT016: { gamut: 'C' }, // Hue bulb A19 LLC005: { gamut: 'A' }, // Living Colors Gen3 Bloom, Aura LLC006: { gamut: 'A' }, // Living Colors Gen3 Iris LLC007: { gamut: 'A' }, // Living Colors Gen3 Bloom, Aura LLC010: { gamut: 'A' }, // Hue Living Colors Iris LLC011: { gamut: 'A' }, // Hue Living Colors Bloom LLC012: { gamut: 'A' }, // Hue Living Colors Bloom LLC013: { gamut: 'A' }, // Disney Living Colors LLC014: { gamut: 'A' }, // Living Colors Gen3 Bloom, Aura LLC020: { gamut: 'C' }, // Hue Go LLM001: { gamut: 'B' }, // Color Light Module LST001: { gamut: 'A' }, // Hue LightStrips LST002: { gamut: 'C' } // Hue LightStrips Plus } }, _TZE200_s8gkrkxk: { // LIDL Xmas lightstrip fix: function () { this.manufacturer = 'LIDL Livarno Lux' this.model = 'HG06467' this.config.colorLoop = false this.config.hs = true } } } knownLights.Philips = knownLights['Signify Netherlands B.V.'] const noResponse = new Error('No Response') noResponse.toString = () => { return noResponse.message } let Service let Characteristic let my class HueLight { static setHomebridge (homebridge, _my, _eve) { Service = homebridge.hap.Service Characteristic = homebridge.hap.Characteristic my = _my } constructor (accessory, id, obj, type = 'light') { this.accessory = accessory this.id = id this.obj = obj this.type = type this.log = this.accessory.log this.serialNumber = this.accessory.serialNumber this.bridge = this.accessory.bridge this.name = obj.name this.resource = '/' + this.type + 's/' + this.id this.key = this.type === 'group' ? 'action' : 'state' this.resourcePath = this.resource + '/' + this.key this.desiredState = {} this.deferrals = [] for (const key in this.obj.action) { if (key !== 'on') { this.obj.state[key] = this.obj.action[key] } } this.hk = {} this.setConfig() this.infoService = this.accessory.getInfoService(this) if (this.config.outlet) { this.service = new Service.Outlet(this.name, this.subtype) this.service.getCharacteristic(Characteristic.OutletInUse) .updateValue(1) } else if (this.config.switch) { this.service = new Service.Switch(this.name, this.subtype) } else if (this.config.valve) { this.service = new Service.Valve(this.name, this.subtype) this.service.getCharacteristic(Characteristic.InUse) .updateValue(0) this.service.getCharacteristic(Characteristic.ValveType) .updateValue(Characteristic.ValveType.GENERIC_VALVE) } else { this.service = new Service.Lightbulb(this.name, this.subtype) } if (this.config.valve) { this.service.getCharacteristic(Characteristic.Active) .on('get', this.getActive.bind(this)) .on('set', this.setActive.bind(this)) this.checkActive(this.obj.state.on) this.hk.duration = 0 this.service.getCharacteristic(Characteristic.SetDuration) .setProps({ maxValue: 4 * 3600 }) .updateValue(this.hk.duration) .on('set', this.setDuration.bind(this)) this.hk.autoInActive = 0 this.service.getCharacteristic(Characteristic.RemainingDuration) .setProps({ maxValue: 4 * 3600 }) .updateValue(0) .on('get', this.getRemainingDuration.bind(this)) } else { this.service.getCharacteristic(Characteristic.On) .on('get', this.getOn.bind(this)) .on('set', this.setOn.bind(this)) if (this.type === 'group') { if (this.bridge.platform.config.anyOn) { this.anyOnKey = 'any_on' this.AnyOnCharacteristic = my.Characteristics.AnyOn this.service.addOptionalCharacteristic(this.AnyOnCharacteristic) this.service.getCharacteristic(this.AnyOnCharacteristic) .on('set', this.setAnyOn.bind(this)) this.checkAllOn(this.obj.state.all_on) } else { this.anyOnKey = 'on' this.AnyOnCharacteristic = Characteristic.On this.checkAllOn = () => {} } this.checkAnyOn(this.obj.state.any_on) if (this.config.streaming) { this.service.addOptionalCharacteristic(my.Characteristics.Streaming) this.service.getCharacteristic(my.Characteristics.Streaming) .on('set', this.setStreaming.bind(this)) this.checkStreaming(this.obj.stream.active) } } else { this.checkOn(this.obj.state.on) } if (this.config.bri) { this.service.getCharacteristic(Characteristic.Brightness) .on('set', this.setBri.bind(this)) this.checkBri(this.obj.state.bri) this.service.addOptionalCharacteristic(my.Characteristics.BrightnessChange) this.service.getCharacteristic(my.Characteristics.BrightnessChange) .updateValue(0) .on('set', this.setBriChange.bind(this)) } if (this.config.ct) { this.colorTemperatureCharacteristic = Characteristic.ColorTemperature this.service.addOptionalCharacteristic(Characteristic.ColorTemperature) this.service.getCharacteristic(Characteristic.ColorTemperature) .updateValue(this.config.minCt) .setProps({ minValue: this.config.minCt, maxValue: this.config.maxCt }) .on('set', this.setCt.bind(this)) this.checkCt(this.obj.state.ct) this.service.addOptionalCharacteristic( Characteristic.SupportedCharacteristicValueTransitionConfiguration ) this.service.getCharacteristic( Characteristic.SupportedCharacteristicValueTransitionConfiguration ) .on('get', this.getSupportedTransitionConfiguration.bind(this)) this.service.addOptionalCharacteristic( Characteristic.CharacteristicValueTransitionControl ) this.service.getCharacteristic( Characteristic.CharacteristicValueTransitionControl ) .on('get', this.getTransitionControl.bind(this)) .on('set', this.setTransitionControl.bind(this)) this.service.addOptionalCharacteristic( Characteristic.CharacteristicValueActiveTransitionCount ) this.service.getCharacteristic( Characteristic.CharacteristicValueActiveTransitionCount ) .updateValue(0) } if (this.config.xy || this.config.hs) { this.service.getCharacteristic(Characteristic.Hue) .on('set', this.setHue.bind(this)) this.service.getCharacteristic(Characteristic.Saturation) .on('set', this.setSat.bind(this)) if (this.config.xy) { this.checkXy(this.obj.state.xy) } else { this.checkHue(this.obj.state.hue) this.checkSat(this.obj.state.sat) } } if (this.config.colorLoop) { this.service.addOptionalCharacteristic(my.Characteristics.ColorLoop) this.service.getCharacteristic(my.Characteristics.ColorLoop) .on('set', this.setColorLoop.bind(this)) } } if (this.bridge.platform.config.configuredName) { this.service.addCharacteristic(Characteristic.ConfiguredName) } if (this.type === 'light') { this.service.addOptionalCharacteristic(Characteristic.StatusFault) this.checkReachable(this.obj.state.reachable) // if (this.bridge.config.nativeHomeKitLights) { // this.service.addOptionalCharacteristic(my.Characteristics.UniqueID) // this.service.getCharacteristic(my.Characteristics.UniqueID) // .updateValue(this.obj.uniqueid) // } } if (this.bridge.platform.config.resource) { this.service.addOptionalCharacteristic(my.Characteristics.Resource) this.service.getCharacteristic(my.Characteristics.Resource) .updateValue(this.resource) } } // Store configuration to this.config. setConfig () { for (const key in this.obj.action) { if (key !== 'on') { this.obj.state[key] = this.obj.action[key] } } this.config = { on: this.obj.state.on !== undefined || this.obj.state.any_on !== undefined, bri: this.obj.state.bri !== undefined, ct: this.obj.state.ct !== undefined, xy: this.obj.state.xy !== undefined, alert: this.obj.state.alert !== undefined, colorLoop: this.obj.state.effect !== undefined, wallSwitch: false, resetTimeout: this.bridge.platform.config.resetTimeout, waitTimeUpdate: this.bridge.platform.config.waitTimeUpdate } if (this.bridge.outlet[this.type + 's'][this.id]) { this.config.outlet = true } else if (this.bridge.switch[this.type + 's'][this.id]) { this.config.switch = true } else if (this.type === 'light' && this.bridge.valve[this.id]) { this.config.valve = true } if (this.config.outlet || this.config.switch || this.config.valve) { this.config.bri = false this.config.ct = false this.config.xy = false } else if (this.type === 'light') { this.config.wallSwitch = this.bridge.platform.config.wallSwitch || this.bridge.wallswitch[this.id] } if (this.config.ct) { // Default colour temperature range: 153 (~6500K) - 500 (2000K). this.config.minCt = 153 this.config.maxCt = 500 } if (this.config.xy) { this.config.gamut = defaultGamut } if (this.type === 'group') { this.manufacturer = this.bridge.obj.manufacturername this.model = this.obj.type if (this.accessory.isMulti) { this.subtype = 'G' + this.id } this.version = this.bridge.version if (this.obj.type === 'Entertainment') { this.config.streaming = true } this.log.debug('%s: %s: config: %j', this.bridge.name, this.resource, this.config) return } this.manufacturer = this.obj.manufacturername this.model = this.obj.modelid if (this.accessory.isMulti) { this.subtype = 'L' + this.id } else { this.subtype = this.obj.uniqueid.split('-')[1] } this.version = this.obj.swversion this.config.unknown = knownLights[this.obj.manufacturername] == null const manufacturer = knownLights[this.obj.manufacturername] || {} manufacturer.models = manufacturer.models || {} const model = manufacturer.models[this.obj.modelid] || {} if (typeof model.fix === 'function') { model.fix.call(this) } else if (typeof manufacturer.fix === 'function') { manufacturer.fix.call(this) } if (this.config.ct) { if (model.minCt != null) { // whitelisted model this.config.minCt = model.minCt } else if (manufacturer.minCt != null) { // whitelisted manufacturer default this.config.minCt = manufacturer.minCt } else if ( this.obj.capabilities != null && this.obj.capabilities.control != null && this.obj.capabilities.control.ct != null && this.obj.capabilities.control.ct.min !== 0 ) { // reported by Hue bridge this.config.minCt = this.obj.capabilities.control.ct.min } if (model.maxCt != null) { // whitelisted model this.config.maxCt = model.maxCt } else if (manufacturer.maxCt != null) { // whitelisted manufacturer default this.config.maxCt = manufacturer.maxCt } else if ( this.obj.capabilities != null && this.obj.capabilities.control != null && this.obj.capabilities.control.ct != null && this.obj.capabilities.control.ct.max !== 0 && this.obj.capabilities.control.ct.max !== 65535 ) { // reported by Hue bridge this.config.maxCt = this.obj.capabilities.control.ct.max } else if (this.config.unknown) { this.log.warn( '%s: %s: warning: using default colour temperature range for unknown light model %j', this.bridge.name, this.resource, this.obj ) } } if (this.config.xy) { if ( model.gamut != null && manufacturer.gamuts != null && manufacturer.gamuts[model.gamut] != null ) { // whitelisted model this.config.gamut = manufacturer.gamuts[model.gamut] } else if (model.gamut != null) { this.config.gamut = model.gamut } else if (manufacturer.gamut != null) { // whitelisted manufacturer default this.config.gamut = manufacturer.gamut } else if ( this.obj.capabilities != null && this.obj.capabilities.control != null && this.obj.capabilities.control.colorgamut != null ) { // reported by Hue bridge const gamut = this.obj.capabilities.control.colorgamut this.config.gamut = { r: [ Math.min(gamut[0][0], defaultGamut.r[0]), Math.max(gamut[0][1], defaultGamut.r[1]) ], g: [ Math.max(gamut[1][0], defaultGamut.g[0]), Math.min(gamut[1][1], defaultGamut.g[1]) ], b: [ Math.max(gamut[2][0], defaultGamut.b[0]), Math.max(gamut[2][1], defaultGamut.b[1]) ] } } else if (this.config.unknown) { this.log.warn( '%s: %s: warning: using default colour gamut for unknown light model %j', this.bridge.name, this.resource, this.obj ) } if (model.computesXy) { this.config.computesXy = model.computesXy } else if (manufacturer.computesXy) { this.config.computesXy = manufacturer.computesXy } } if (model.noAlert) { this.config.noAlert = true } if (model.noTransition) { this.config.noTransition = model.noTransition } else if (manufacturer.noTransition) { this.config.noTransition = manufacturer.noTransition } if (model.noWallSwitch) { this.config.wallSwitch = false } this.log.debug('%s: %s: config: %j', this.bridge.name, this.resource, this.config) } // ===== Bridge Events ========================================================= heartbeat (beat, obj) { if (this.updating) { return } // this.checkName(obj.name) for (const key in obj.action) { if (key !== 'on') { obj.state[key] = obj.action[key] } } this.checkState(obj.state) if (this.config.streaming) { this.checkStreaming(obj.stream.active) } if (beat % 60 === 0) { this.checkAdaptiveLighting() } } checkState (state, event) { for (const key in state) { switch (key) { case 'alert': break case 'all_on': this.checkAllOn(state.all_on) break case 'any_on': this.checkAnyOn(state.any_on) break case 'bri': this.checkBri(state.bri) break case 'colormode': this.obj.state.colormode = state.colormode break case 'ct': this.checkCt(state.ct) break case 'effect': this.checkEffect(state.effect) break case 'hue': this.checkHue(state.hue) break case 'mode': break case 'on': if (this.config.valve) { this.checkActive(state.on) } else { this.checkOn(state.on) } break case 'reachable': this.checkReachable(state.reachable) break case 'sat': this.checkSat(state.sat) break case 'scene': break case 'xy': this.checkXy(state.xy) break default: this.log.debug( '%s: ignore unknown attribute state.%s', this.name, key ) break } } } checkOn (on) { if (this.obj.state.on !== on) { this.log.debug( '%s: %s on changed from %s to %s', this.name, this.type, this.obj.state.on, on ) this.obj.state.on = on } if (this.config.doorLock) { const hkLockState = on ? Characteristic.LockCurrentState.SECURED : Characteristic.LockCurrentState.UNSECURED if (this.hk.lockCurrentState !== hkLockState) { if (this.hk.lockCurrentState !== undefined) { this.log.info( '%s: set homekit lock current state from %s to %s', this.name, this.hk.lockCurrentState, hkLockState ) } } this.hk.lockCurrentState = hkLockState this.service.getCharacteristic(Characteristic.LockCurrentState) .updateValue(this.hk.lockCurrentState) return } let hkOn if (this.config.wallSwitch && !this.obj.state.reachable) { if (this.hk.on) { this.log.info('%s: not reachable: force homekit on to false', this.name) } hkOn = false } else { hkOn = this.obj.state.on } if (this.hk.on !== hkOn) { if (this.hk.on !== undefined) { this.log.info( '%s: set homekit on from %s to %s', this.name, this.hk.on, hkOn ) } this.hk.on = hkOn this.service.getCharacteristic(Characteristic.On) .updateValue(this.hk.on) this.checkAdaptiveLighting() } } checkAllOn (allOn) { if (this.obj.state.all_on !== allOn) { this.log.debug( '%s: %s all_on changed from %s to %s', this.name, this.type, this.obj.state.all_on, allOn ) this.obj.state.all_on = allOn } const hkOn = this.obj.state.all_on if (this.hk.on !== hkOn) { if (this.hk.on !== undefined) { this.log.info( '%s: set homekit on from %s to %s', this.name, this.hk.on, hkOn ) } this.hk.on = hkOn this.service.getCharacteristic(Characteristic.On) .updateValue(this.hk.on) } } checkAnyOn (anyOn) { if (this.obj.state[this.anyOnKey] !== anyOn) { this.log.debug( '%s: %s any_on changed from %s to %s', this.name, this.type, this.obj.state[this.anyOnKey], anyOn ) this.obj.state[this.anyOnKey] = anyOn } const hkAnyOn = this.obj.state[this.anyOnKey] if (this.hk[this.anyOnKey] !== hkAnyOn) { if (this.hk[this.anyOnKey] !== undefined) { this.log.info( '%s: set homekit any on from %s to %s', this.name, this.hk[this.anyOnKey], hkAnyOn ) } this.hk[this.anyOnKey] = hkAnyOn this.service.getCharacteristic(this.AnyOnCharacteristic) .updateValue(this.hk[this.anyOnKey]) } } checkStreaming (streaming) { if (this.obj.state.streaming !== streaming) { this.log.debug( '%s: streaming changed from %s to %s', this.name, this.obj.state.streaming, streaming ) this.obj.state.streaming = streaming } const hkStreaming = this.obj.state.streaming if (this.hk.streaming !== hkStreaming) { if (this.hk.streaming !== undefined) { this.log.info( '%s: set homekit streaming from %s to %s', this.name, this.hk.streaming, hkStreaming ) } this.hk.streaming = hkStreaming this.service.getCharacteristic(my.Characteristics.Streaming) .updateValue(this.hk.streaming) } } checkActive (on) { if (this.obj.state.on !== on) { this.log.debug( '%s: %s on changed from %s to %s', this.name, this.type, this.obj.state.on, on ) this.obj.state.on = on } const hkActive = this.obj.state.on ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE if (this.hk.active !== hkActive) { if (this.hk.active !== undefined) { this.log.info( '%s: set homekit active from %s to %s', this.name, this.hk.active, hkActive ) } this.hk.active = hkActive this.service.getCharacteristic(Characteristic.Active) .updateValue(this.hk.active) this.didSetActive() } } checkBri (bri) { if (!this.config.bri) { return } if (this.obj.state.bri !== bri) { this.log.debug( '%s: %s bri changed from %s to %s', this.name, this.type, this.obj.state.bri, bri ) if (this.recentlyUpdated) { this.log.debug('%s: recently updated - ignore changed bri', this.name) return } this.obj.state.bri = bri } const hkBri = Math.round(this.obj.state.bri * 100.0 / 254.0) if (this.hk.bri !== hkBri) { if (this.hk.bri !== undefined) { this.log.info( '%s: set homekit brightness from %s%% to %s%%', this.name, this.hk.bri, hkBri ) } this.hk.bri = hkBri this.service.getCharacteristic(Characteristic.Brightness) .updateValue(this.hk.bri) this.checkAdaptiveLighting() } } checkCt (ct) { if (!this.config.ct || this.obj.state.colormode !== 'ct') { return } if (this.obj.state.ct !== ct) { this.log.debug( '%s: %s ct changed from %s to %s', this.name, this.type, this.obj.state.ct, ct ) // this.disableAdaptiveLighting() if (this.recentlyUpdated) { this.log.debug('%s: recently updated - ignore changed ct', this.name) return } this.obj.state.ct = ct } const hkCt = Math.max(this.config.minCt, Math.min(this.config.maxCt, ct)) if (this.hk.ct !== hkCt) { if (this.hk.ct !== undefined) { this.log.info( '%s: set homekit color temperature from %s mired to %s mired', this.name, this.hk.ct, hkCt ) } this.hk.ct = hkCt this.service.getCharacteristic(Characteristic.ColorTemperature) .updateValue(this.hk.ct) } } checkHue (hue) { if (!this.config.hs || this.obj.state.colormode !== 'hs') { return } if (this.obj.state.hue !== hue) { this.log.debug( '%s: %s hue changed from %s to %s', this.name, this.type, this.obj.state.hue, hue ) this.obj.state.hue = hue } const hkHue = Math.round(this.obj.state.hue * 360.0 / 65535.0) if (this.hk.hue !== hkHue) { if (this.hk.hue !== undefined) { this.log.info( '%s: set homekit hue from %s˚ to %s˚', this.name, this.hk.hue, hkHue ) } this.hk.hue = hkHue this.service.getCharacteristic(Characteristic.Hue) .updateValue(this.hk.hue) } } checkSat (sat) { if (!this.config.hs || this.obj.state.colormode !== 'hs') { return } if (this.obj.state.sat !== sat) { this.log.debug( '%s: %s sat changed from %s to %s', this.name, this.type, this.obj.state.sat, sat ) this.obj.state.sat = sat } const hkSat = Math.round(this.obj.state.sat * 100.0 / 254.0) if (this.hk.sat !== hkSat) { if (this.hk.sat !== undefined) { this.log.info( '%s: set homekit saturation from %s%%to %s%%', this.name, this.hk.sat, hkSat ) } this.hk.sat = hkSat this.service.getCharacteristic(Characteristic.Saturation) .updateValue(this.hk.sat) } } checkEffect (effect) { if (!this.config.colorLoop) { return } if (this.obj.state.effect !== effect) { this.log.debug( '%s: %s effect changed from %s to %s', this.name, this.type, this.obj.state.effect, effect ) this.disableAdaptiveLighting() this.obj.state.effect = effect } if (this.config.colorLoop) { const hkColorloop = effect === 'colorloop' if (this.hk.colorLoop !== hkColorloop) { if (this.hk.colorLoop !== undefined) { this.log.info( '%s: set homekit color loop from %s to %s', this.name, this.hk.colorLoop, hkColorloop ) } this.hk.colorLoop = hkColorloop this.service.getCharacteristic(my.Characteristics.ColorLoop) .updateValue(this.hk.colorLoop) } } } checkReachable (reachable) { if (this.obj.state.reachable !== reachable) { this.log.debug( '%s: %s reachable changed from %j to %j', this.name, this.type, this.obj.state.reachable, reachable ) this.obj.state.reachable = reachable } const hkFault = this.obj.state.reachable ? 0 : 1 if (this.hk.fault !== hkFault) { if (this.hk.fault !== undefined) { this.log.info( '%s: set homekit status fault from %s to %s', this.name, this.hk.fault, hkFault ) } this.hk.fault = hkFault this.service.getCharacteristic(Characteristic.StatusFault) .updateValue(this.hk.fault) } if (this.config.wallSwitch && !this.config.valvePosition) { this.checkOn(this.obj.state.on) } } checkXy (xy, fromCt = false) { if (!this.config.xy) { return } if (this.obj.state.xy[0] !== xy[0] || this.obj.state.xy[1] !== xy[1]) { if (fromCt) { this.log.debug( '%s: %s xy predicted to change by ct from %j to %j', this.name, this.type, this.obj.state.xy, xy ) } else if (this.obj.state.colormode === 'xy') { this.log.debug( '%s: %s xy changed from %j to %j', this.name, this.type, this.obj.state.xy, xy ) this.disableAdaptiveLighting() } else { this.log.debug( '%s: %s xy changed by %s from %j to %j', this.name, this.type, this.obj.state.colormode, this.obj.state.xy, xy ) } if (this.recentlyUpdated && !fromCt) { this.log.debug('%s: recently updated - ignore changed xy', this.name) return } this.obj.state.xy = xy } if (this.obj.state.colormode !== 'ct' || fromCt || this.config.computesXy) { const { h, s } = xyToHsv(this.obj.state.xy, this.config.gamut) if (this.hk.hue !== h) { if (this.hk.hue !== undefined) { this.log.info( '%s: set homekit hue from %s˚ to %s˚', this.name, this.hk.hue, h ) } this.hk.hue = h this.service.getCharacteristic(Characteristic.Hue) .updateValue(this.hk.hue) } if (this.hk.sat !== s) { if (this.hk.sat !== undefined) { this.log.info( '%s: set homekit saturation from %s%% to %s%%', this.name, this.hk.sat, s ) } this.hk.sat = s this.service.getCharacteristic(Characteristic.Saturation) .updateValue(this.hk.sat) } } } // ===== Homekit Events ======================================================== identify (callback) { this.log.debug('%s: %s: config: %j', this.bridge.name, this.resource, this.config) this.log.info('%s: identify', this.name) if (!this.config.alert) { return callback() } let alert = 'select' let stop if (this.bridge.type === 'bridge') { if (this.config.noAlert) { return callback() } } else if (this.manufacturer === this.bridge.philips) { alert = 'breathe' stop = 'stop' } this.put({ alert }).then((obj) => { if (stop != null) { setTimeout(() => { this.put({ alert: stop }) }, 1500) } return callback() }).catch((error) => { return callback(error) }) } getOn (callback) { if (this.bridge.platform.config.noResponse && !this.obj.state.reachable) { return callback(noResponse) } return callback(null, this.hk.on) } setOn (on, callback) { if (on === this.hk.on) { return callback() } this.log.info( '%s: homekit on changed from %s to %s', this.name, this.hk.on, on ) const oldOn = this.hk.on this.hk.on = on const newOn = this.hk.on const request = { on: newOn } this.checkAdaptiveLighting() this.put(request).then(() => { if (this.type === 'group') { this.obj.state[this.anyOnKey] = newOn this.obj.state.all_on = newOn } else { this.obj.state.on = newOn } callback() }).catch((error) => { this.hk.on = oldOn callback(error) }) } setAnyOn (anyOn, callback) { if (anyOn === this.hk[this.anyOnKey]) { return callback() } this.log.info( '%s: homekit any on changed from %s to %s', this.name, this.hk[this.anyOnKey], anyOn ) const oldAnyOn = this.hk[this.anyOnKey] this.hk[this.anyOnKey] = anyOn const newOn = this.hk[this.anyOnKey] this.put({ on: newOn }).then(() => { this.obj.state[this.anyOnKey] = newOn this.obj.state.all_on = newOn callback() }).catch((error) => { this.hk[this.anyOnKey] = oldAnyOn callback(error) }) } setStreaming (streaming, callback) { if (streaming === this.hk.streaming) { return callback() } this.log.info( '%s: homekit streaming changed from %s to %s', this.name, this.hk.streaming, streaming ) const oldStreaming = this.hk.streaming this.hk.streaming = streaming const newStreaming = this.hk.streaming this.bridge.put(this.resource, { stream: { active: newStreaming } }).then((obj) => { this.obj.state.streaming = newStreaming return callback() }).catch((error) => { this.hk.streaming = oldStreaming return callback(error) }) } getActive (callback) { if (this.bridge.platform.config.noResponse && !this.obj.state.reachable) { return callback(noResponse) } return callback(null, this.hk.active) } setActive (active, callback) { if (active === this.hk.active) { return callback() } this.log.info( '%s: homekit active changed from %s to %s', this.name, this.hk.active, active ) const oldActive = this.hk.active this.hk.active = active const newOn = this.hk.active === Characteristic.Active.ACTIVE this.put({ on: newOn }).then(() => { this.obj.state.on = newOn callback() this.didSetActive() }).catch((error) => { this.hk.active = oldActive callback(error) }) } setBri (bri, callback) { // if (bri === this.hk.bri) { // return callback() // } this.log.info( '%s: homekit brightness changed from %s%% to %s%%', this.name, this.hk.bri, bri ) const oldBri = this.hk.bri this.hk.bri = bri this.checkAdaptiveLighting() const newBri = Math.round(this.hk.bri * 254.0 / 100.0) this.put({ bri: newBri }).then(() => { this.obj.state.bri = newBri callback() }).catch((error) => { this.hk.bri = oldBri callback(error) }) } setBriChange (delta, callback) { delta = Math.round(delta) if (delta === 0) { return callback() } this.log.info( '%s: homekit brightness change by %s%%', this.name, delta ) const briDelta = Math.round(delta * 254.0 / 100.0) this.put({ bri_inc: briDelta }).then((obj) => { setTimeout(() => { this.service.setCharacteristic(my.Characteristics.BrightnessChange, 0) }, this.config.resetTimeout) callback() }).catch((error) => { callback(error) }) } setCt (ct, callback) { if (ct === this.hk.ct && this.obj.state.colormode === 'ct') { return callback() } this.log.info( '%s: homekit color temperature changed from %s mired to %s mired', this.name, this.hk.ct, ct ) this.disableAdaptiveLighting() const oldCt = this.hk.ct this.hk.ct = ct const newCt = this.hk.ct this.put({ ct: newCt }).then(() => { this.obj.state.ct = newCt this.obj.state.colormode = 'ct' this.checkXy(ctToXy(this.obj.state.ct), true) callback() }).catch((error) => { this.hk.ct = oldCt callback(error) }) } setDuration (duration, callback) { if (duration === this.hk.duration) { return callback() } this.log.info( '%s: homekit duration changed from %ss to %ss', this.name, this.hk.duration, duration ) this.hk.duration = duration callback() } setHue (hue, callback) { if (hue === this.hk.hue && this.obj.state.colormode === 'xy') { return callback() } this.log.info( '%s: homekit hue changed from %s˚ to %s˚', this.name, this.hk.hue, hue ) this.disableAdaptiveLighting() const oldHue = this.hk.hue this.hk.hue = hue if (this.config.xy && this.hk.sat != null) { const newXy = hsvToXy(this.hk.hue, this.hk.sat, this.config.gamut) this.put({ xy: newXy }).then(() => { this.obj.state.xy = newXy this.obj.state.colormode = 'xy' callback() }).catch((error) => { this.hk.hue = oldHue callback(error) }) } else if (this.config.hs) { const newHue = Math.round(this.hk.hue * 65535.0 / 360.0) this.put({ hue: newHue }).then(() => { this.obj.state.hue = newHue this.obj.state.colormode = 'hs' callback() }).catch((error) => { this.hk.hue = oldHue callback(error) }) } } setSat (sat, callback) { if (sat === this.hk.sat && this.obj.state.colormode === 'xy') { return callback() } this.log.info( '%s: homekit saturation changed from %s%% to %s%%', this.name, this.hk.sat, sat ) this.disableAdaptiveLighting() const oldSat = this.hk.sat this.hk.sat = sat if (this.config.xy && this.hk.hue != null) { const newXy = hsvToXy(this.hk.hue, this.hk.sat, this.config.gamut) this.put({ xy: newXy }).then(() => { this.obj.state.xy = newXy this.obj.state.colormode = 'xy' callback() }).catch((error) => { this.hk.sat = oldSat callback(error) }) } else if (this.config.hs) { const newSat = Math.round(this.hk.sat * 254.0 / 100.0) this.put({ sat: newSat }).then(() => { this.obj.state.sat = newSat this.obj.state.colormode = 'hs' callback() }).catch((error) => { this.hk.sat = oldSat callback(error) }) } } setColorLoop (colorLoop, callback) { if (colorLoop === this.hk.colorLoop) { return callback() } this.log.info( '%s: homekit color loop changed from %s to %s', this.name, this.hk.colorLoop, colorLoop ) this.disableAdaptiveLighting() const oldColorLoop = this.hk.colorLoop this.hk.colorLoop = colorLoop const state = { effect: this.hk.colorLoop ? 'colorloop' : 'none' } this.put(state).then(() => { this.obj.state.effect = state.effect callback() }).catch((error) => { this.hk.colorLoop = oldColorLoop callback(error) }) } getRemainingDuration (callback) { let remaining = this.hk.autoInActive - new Date().valueOf() remaining = remaining > 0 ? Math.round(remaining / 1000) : 0 this.log.info('%s: remaining duration %ss', this.name, remaining) callback(null, remaining) } didSetActive () { if (this.hk.duration > 0) { if (this.hk.active) { this.hk.autoInActive = new Date().valueOf() + this.hk.duration * 1000 this.hk.autoInActiveTimeout = setTimeout(() => { this.log.debug('%s: remaining duration 0s', this.name) this.put({ on: false }).then(() => { }).catch((error) => { this.log.warn('%s: error %s', this.name, formatError(error)) }) }, this.hk.duration * 1000) } else { if (this.hk.autoInActiveTimeout != null) { clearTimeout(this.hk.autoInActiveTimeout) delete this.hk.autoInActiveTimeout } } } setTimeout(() => { this.log.info('%s: set homekit in use to %s', this.name, this.hk.active) this.service.updateCharacteristic(Characteristic.InUse, this.hk.active) if (this.hk.active && this.hk.duration > 0) { this.log.info( '%s: set homekit remaining duration to %ss', this.name, this.hk.duration ) this.service.updateCharacteristic( Characteristic.RemainingDuration, this.hk.duration ) } else if (this.hk.autoInActive !== 0) { this.hk.autoInActive = 0 this.log.info('%s: set homekit remaining duration to 0s', this.name) this.service.updateCharacteristic(Characteristic.RemainingDuration, 0) } }, 500) } getSupportedTransitionConfiguration (callback) { try { // The SuppotedTransitionConfiguration value is constant, but the iid values // for the characteristics are only assigned when the HAP server is started, // so we cannot set the value while defining the service. const bri = this.service.getCharacteristic(Characteristic.Brightness).iid const ct = this.service.getCharacteristic(Characteristic.ColorTemperature).iid this.log.debug( '%s: brightness idd: %d, color temperature idd: %d', this.name, bri, ct ) this.al = new AdaptiveLighting(bri, ct) const configuration = this.al.generateConfiguration() this.log.debug( '%s: set homekit supported transition configuration to %s', this.name, configuration ) this.log.info( '%s: set homekit supported transition configuration to %j', this.name, this.al.parseConfiguration(configuration) ) // Remove the event handler, since we now have the value. this.service.getCharacteristic(Characteristic.SupportedCharacteristicValueTransitionConfiguration) .setValue(configuration) .removeAllListeners('get') callback(null, configuration) } catch (error) { this.log.warn( '%s: cannot compute supported transition configuration: %s', this.name, formatError(error) ) callback(error) } } getTransitionControl (callback) { try { if (this.al == null) { return callback(null, '') } const control = this.al.generateControl() this.log.debug( '%s: set homekit transition control to %j', this.name, control ) if (control !== '') { this.log.info( '%s: set homekit transition control to %j', this.name, this.al.parseControl(control) ) } callback(null, control) } catch (error) { this.log.warn( '%s: cannot compute transition control: %s', this.name, formatError(error) ) callback(error) } } setTransitionControl (control, callback) { try { this.log.debug( '%s: homekit transition control set to %j', this.name, control ) this.log.info( '%s: homekit transition control set to %j', this.name, this.al.parseControl(control) ) const controlResponse = this.al.generateControlResponse() this.log.debug( '%s: set homekit transition control to %j', this.name, controlResponse ) this.log.info( '%s: set homekit transition control to %j', this.name, this.al.parseControl(controlResponse) ) this.log.info('%s: set homekit active transition count to 1', this.name) this.service.getCharacteristic( Characteristic.CharacteristicValueActiveTransitionCount ) .updateValue(1) this.checkAdaptiveLighting() callback(null, controlResponse) } catch (error) { this.log.warn( '%s: cannot handle transition control: %s', this.name, formatError(error) ) } } checkAdaptiveLighting () { if (this.al == null || !this.hk.on) { return } const ct = this.al.getCt( this.hk.bri * this.bridge.platform.config.brightnessAdjustment ) if (ct == null) { return } if (ct !== this.hk.ct || this.obj.state.colormode !== 'ct') { this.log.info( '%s: homekit adaptive lighting color temperature changed from %s mired to %s mired', this.name, this.hk.ct, ct ) const oldCt = this.hk.ct this.hk.ct = ct const newCt = this.hk.ct this.put({ ct: newCt }).then(() => { this.obj.state.ct = newCt this.obj.state.colormode = 'ct' this.checkXy(ctToXy(this.obj.state.ct), true) }).catch(() => { this.hk.ct = oldCt }) } } disableAdaptiveLighting () { if (this.al != null && this.al.active && !this.recentlyUpdated) { this.al.deactivate() this.log.info('%s: set homekit active transition count to 0', this.name) this.service.getCharacteristic( Characteristic.CharacteristicValueActiveTransitionCount ) .updateValue(0) } } // Collect changes into a combined request. put (state) { return new Promise((resolve, reject) => { for (const key in state) { this.desiredState[key] = state[key] } const d = { resolve, reject } this.deferrals.push(d) if (this.updating) { return } this.updating = true if (this.config.waitTimeUpdate > 0) { setTimeout(() => { this._put() }, this.config.waitTimeUpdate) } else { this._put() } }) } // Send the request (for the combined changes) to the Hue bridge. _put () { const desiredState = this.desiredState const deferrals = this.deferrals this.desiredState = {} this.deferrals = [] this.updating = false if ( this.bridge.state.transitiontime !== this.bridge.defaultTransitiontime && desiredState.transitiontime === undefined ) { desiredState.transitiontime = this.bridge.state.transitiontime * 10 this.bridge.resetTransitionTime() } if (this.config.noTransition) { if ( ( desiredState.on != null || desiredState.bri != null || desiredState.bri_inc != null ) && ( desiredState.xy != null || desiredState.ct != null || desiredState.hue != null || desiredState.sat != null || desiredState.effect != null ) ) { desiredState.transitiontime = 0 } } this.bridge.put(this.resourcePath, desiredState).then((obj) => { if (this.bridge.platform.config.noResponse && !this.obj.state.reachable) { this.log.warn('%s: %s not reachable', this.name, this.resource) for (const d of deferrals) { d.reject(noResponse) } return } this.recentlyUpdated = true for (const d of deferrals) { d.resolve(true) } setTimeout(() => { this.recentlyUpdated = false }, 500) }).catch((error) => { for (const d of deferrals) { d.reject(error) } }) } } export { HueLight }