UNPKG

homebridge-hue

Version:
1,401 lines (1,361 loc) 48.9 kB
// homebridge-hue/lib/HueSensor.js // Copyright © 2016-2026 Erik Baauw. All rights reserved. // // Homebridge plugin for Philips Hue. function dateToString (date, utc = true) { if (date == null || date === 'none') { return 'n/a' } if (utc && !date.endsWith('Z')) { date += 'Z' } return String(new Date(date)).slice(0, 24) } function hkLightLevel (v) { let l = v ? Math.pow(10, (v - 1) / 10000) : 0.0001 l = Math.round(l * 10000) / 10000 return l > 100000 ? 100000 : l < 0.0001 ? 0.0001 : l } const PRESS = 0 const HOLD = 1 const SHORT_RELEASE = 2 const LONG_RELEASE = 3 // As homebridge-hue polls the Hue bridge, not all dimmer switch buttonevents // are received reliably. Consequently, we only issue one HomeKit change per // Press/Hold/Release event series. function hkZLLSwitchAction (value, oldValue, repeat = false) { if (value < 1000) { return Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS } const button = Math.floor(value / 1000) const oldButton = Math.floor(oldValue / 1000) const event = value % 1000 const oldEvent = oldValue % 1000 switch (event) { case PRESS: // Wait for Hold or Release after press. return null case SHORT_RELEASE: return Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS case HOLD: case LONG_RELEASE: if (repeat) { return Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS } if (button === oldButton && oldEvent === HOLD) { // Already issued action on previous Hold. return null } return Characteristic.ProgrammableSwitchEvent.LONG_PRESS default: return null } } // Link this module to homebridge. let Service let Characteristic let my let eve // let HistoryService let SINGLE let SINGLE_LONG class HueSensor { static setHomebridge (homebridge, _my, _eve) { Service = homebridge.hap.Service Characteristic = homebridge.hap.Characteristic my = _my eve = _eve SINGLE = { minValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, maxValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, validValues: [ Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS ] } SINGLE_LONG = { minValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, maxValue: Characteristic.ProgrammableSwitchEvent.LONG_PRESS, validValues: [ Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, Characteristic.ProgrammableSwitchEvent.LONG_PRESS ] } } constructor (accessory, id, obj) { this.accessory = accessory this.id = id this.obj = obj this.bridge = this.accessory.bridge this.log = this.accessory.log this.serialNumber = this.accessory.serialNumber this.name = this.obj.name this.hk = {} this.resource = '/sensors/' + id this.serviceList = [] if (this.obj.type[0] === 'Z') { // Zigbee sensor. this.manufacturer = this.obj.manufacturername this.model = this.obj.modelid this.endpoint = this.obj.uniqueid.split('-')[1] this.cluster = this.obj.uniqueid.split('-')[2] this.subtype = this.endpoint + '-' + this.cluster this.version = this.obj.swversion } else { // Hue bridge internal sensor. this.manufacturer = this.bridge.manufacturer if (this.accessory.isMulti) { this.model = 'MultiCLIP' this.subtype = this.id } else if ( this.obj.manufacturername === 'homebridge-hue' && this.obj.modelid === this.obj.type && this.obj.uniqueid.split('-')[1] === this.id ) { // Combine multiple CLIP sensors into one accessory. this.model = 'MultiCLIP' this.subtype = this.id } else { this.model = this.obj.type } this.version = this.bridge.version } this.infoService = this.accessory.getInfoService(this) let durationKey = 'duration' switch (this.obj.type) { case 'ZGPSwitch': case 'ZLLSwitch': { this.buttonMap = {} let namespace = Characteristic.ServiceLabelNamespace.ARABIC_NUMERALS switch (this.obj.manufacturername) { case 'Lutron': switch (this.obj.modelid) { case 'Z3-1BRL': // Lutron Aurora, see #522. this.createButton(1, 'Button', SINGLE_LONG) break default: break } break case 'Philips': case 'Signify Netherlands B.V.': { const repeat = this.bridge.platform.config.hueDimmerRepeat const events = repeat ? SINGLE : SINGLE_LONG switch (this.obj.modelid) { case 'RDM001': // Hue wall switch module case 'RDM004': // Hue wall switch module switch (obj.config.devicemode) { case 'singlerocker': this.createButton(1, 'Rocker 1', SINGLE) break case 'singlepushbutton': this.createButton(1, 'Push Button 1', events) if (repeat) this.repeat = [1] break case 'dualrocker': this.createButton(1, 'Rocker 1', SINGLE) this.createButton(2, 'Rocker 2', SINGLE) break case 'dualpushbutton': this.createButton(1, 'Push Button 1', events) this.createButton(2, 'Push Button 2', events) if (repeat) this.repeat = [1, 2] break default: break } break case 'RDM002': // Hue tap dial switch namespace = Characteristic.ServiceLabelNamespace.DOTS this.createButton(1, '1', events) // On/Off this.createButton(2, '2', events) this.createButton(3, '3', events) this.createButton(4, '4', events) // Hue if (repeat) this.repeat = [1, 2, 3, 4] break case 'ROM001': // Hue smart button case 'RDM003': // Hue smart button case 'RDM005': // Hue smart button (v2) this.createButton(1, 'Button', events) if (repeat) this.repeat = [1] break case 'RWL020': case 'RWL021': // Hue dimmer switch this.createButton(1, 'On', SINGLE_LONG) this.createButton(2, 'Dim Up', events) this.createButton(3, 'Dim Down', events) this.createButton(4, 'Off', SINGLE_LONG) if (repeat) this.repeat = [2, 3] break case 'RWL022': // Hue dimmer switch (2021) this.createButton(1, 'On', SINGLE_LONG) // On/Off this.createButton(2, 'Dim Up', events) this.createButton(3, 'Dim Down', events) this.createButton(4, 'Off', SINGLE_LONG) // Hue if (repeat) this.repeat = [2, 3] break case 'ZGPSWITCH': // Hue tap namespace = Characteristic.ServiceLabelNamespace.DOTS this.createButton(1, '1', SINGLE) this.createButton(2, '2', SINGLE) this.createButton(3, '3', SINGLE) this.createButton(4, '4', SINGLE) this.createButton(5, '1 and 2', SINGLE) this.createButton(6, '3 and 4', SINGLE) this.convertButtonEvent = (value) => { return { 34: 1002, // Press 1 1000: 1002, 16: 2002, // Press 2 2000: 2002, 17: 3002, // Press 3 3000: 3002, 18: 4002, // Press 4 4000: 4002, 100: 5000, // Press 1 and 2 101: 5002, // Release 1 and 2 98: 6000, // Press 3 and 4 99: 6002 // Release 3 and 4 }[value] } break default: break } break } case 'PhilipsFoH': switch (this.obj.modelid) { case 'FOHSWITCH': { // Friends-of-Hue switch this.createButton(1, 'Top Left', SINGLE) this.createButton(2, 'Bottom Left', SINGLE) this.createButton(3, 'Top Right', SINGLE) this.createButton(4, 'Bottom Right', SINGLE) this.createButton(5, 'Top Both', SINGLE) this.createButton(6, 'Bottom Both', SINGLE) this.convertButtonEvent = (value) => { if (value < 1000) { return { 16: 1000, // Press Top Left 20: 1002, // Release Top Left 17: 2000, // Press Bottom Left 21: 2002, // Release Bottom Left 19: 3000, // Press Top Right 23: 3002, // Relesase Top Right 18: 4000, // Press Botton Right 22: 4002, // Release Bottom Right 100: 5000, // Press Top Both 101: 5002, // Release Top Both 98: 6000, // Press Bottom Both 99: 6002 // Release Bottom Both }[value] } return value } break } default: break } break default: break } if (Object.keys(this.buttonMap).length > 0) { this.createLabel(namespace) this.type = { key: 'buttonevent', homekitValue: (v) => { return Math.floor(v / 1000) }, homekitAction: hkZLLSwitchAction } } else { this.log.warn( '%s: %s: warning: ignoring unknown %s sensor %j', this.bridge.name, this.resource, this.obj.type, this.obj ) } break } case 'ZLLRelativeRotary': { this.buttonMap = {} let namespace = Characteristic.ServiceLabelNamespace.ARABIC_NUMERALS let homekitValue if ( this.obj.manufacturername === 'Signify Netherlands B.V.' && this.obj.modelid === 'RDM002' ) { // Hue tap dial switch namespace = Characteristic.ServiceLabelNamespace.DOTS this.createButton(5, 'Turn Right', SINGLE) this.createButton(6, 'Turn Left', SINGLE) homekitValue = (v) => { return v > 0 ? 5 : 6 } } else if ( this.obj.manufacturername === 'Lutron' && this.obj.modelid === 'Z3-1BRL' ) { // Lutron Aurora, see #522. this.createButton(2, 'Turn Right', SINGLE) this.createButton(3, 'Turn Left', SINGLE) homekitValue = (v) => { return v > 0 ? 2 : 3 } } if (Object.keys(this.buttonMap).length > 0) { this.createLabel(namespace) this.type = { key: 'expectedrotation', homekitValue, homekitAction: () => { return Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS } } } else { this.log.warn( '%s: %s: warning: ignoring unknown %s sensor %j', this.bridge.name, this.resource, this.obj.type, this.obj ) } break } case 'CLIPSwitch': // 2.1 // We'd need a way to specify the number of buttons, cf. max value for // a CLIPGenericStatus sensor. this.log.warn( '%s: %s: warning: ignoring unsupported sensor type %s', this.bridge.name, this.resource, this.obj.type ) break case 'ZLLPresence': if ( ['Philips', 'Signify Netherlands B.V.'].includes(this.obj.manufacturername) && ['SML001', 'SML002', 'SML003', 'SML004'].includes(this.obj.modelid) ) { // 1.3 - Hue motion sensor durationKey = 'delay' } else { this.log.warn( '%s: %s: warning: unknown %s sensor %j', this.bridge.name, this.resource, this.obj.type, this.obj ) } // falls through case 'CLIPPresence': // 2.3 case 'Geofence': // Undocumented this.service = new eve.Services.MotionSensor(this.name, this.subtype) this.serviceList.push(this.service) this.duration = 0 this.type = { Characteristic: Characteristic.MotionDetected, key: 'presence', name: 'motion', unit: '', history: 'motion', homekitValue: (v) => { return v ? 1 : 0 }, durationKey, sensitivitymax: this.obj.config.sensitivitymax } break case 'ZLLTemperature': if ( ['Philips', 'Signify Netherlands B.V.'].includes(this.obj.manufacturername) && ['SML001', 'SML002', 'SML003', 'SML004'].includes(this.obj.modelid) ) { // 1.4 - Hue motion sensor } else { this.log.warn( '%s: %s: warning: unknown %s sensor %j', this.bridge.name, this.resource, this.obj.type, this.obj ) } // falls through case 'CLIPTemperature': // 2.4 this.service = new eve.Services.TemperatureSensor(this.name, this.subtype) this.serviceList.push(this.service) this.type = { Characteristic: Characteristic.CurrentTemperature, key: 'temperature', name: 'temperature', unit: '°C', history: 'weather', homekitValue: (v) => { return v ? Math.round(v / 10) / 10 : 0 } } break case 'ZLLLightLevel': // 2.7 - Hue Motion Sensor if ( ['Philips', 'Signify Netherlands B.V.'].includes(this.obj.manufacturername) && ['SML001', 'SML002', 'SML003', 'SML004'].includes(this.obj.modelid) ) { // 1.4 - Hue motion sensor } else { this.log.warn( '%s: %s: warning: unknown %s sensor %j', this.bridge.name, this.resource, this.obj.type, this.obj ) } // falls through case 'CLIPLightLevel': // 2.7 this.service = new Service.LightSensor(this.name, this.subtype) this.serviceList.push(this.service) this.type = { Characteristic: Characteristic.CurrentAmbientLightLevel, key: 'lightlevel', name: 'light level', unit: ' lux', homekitValue: hkLightLevel } break case 'ZLLOpenClose': this.log.warn( '%s: %s: warning: unknown %s sensor %j', this.bridge.name, this.resource, this.obj.type, this.obj ) // falls through case 'CLIPOpenClose': // 2.2 this.service = new eve.Services.ContactSensor(this.name, this.subtype) this.serviceList.push(this.service) this.type = { Characteristic: Characteristic.ContactSensorState, key: 'open', name: 'contact', unit: '', history: 'door', homekitValue: (v) => { return v ? 1 : 0 } } break case 'Daylight': if ( this.obj.manufacturername === this.bridge.philips && this.obj.modelid === 'PHDL00' ) { // 2.6 - Built-in daylight sensor. if (!this.obj.config.configured) { this.log.warn( '%s: %s: warning: %s sensor not configured', this.bridge.name, this.resource, this.obj.type ) } this.manufacturer = this.obj.manufacturername this.model = this.obj.modelid this.service = new Service.LightSensor(this.name, this.subtype) this.serviceList.push(this.service) this.type = { Characteristic: Characteristic.CurrentAmbientLightLevel, key: 'lightlevel', name: 'light level', unit: ' lux', homekitValue: hkLightLevel } if (obj.state.status == null) { // Hue bridge obj.state.lightlevel = obj.state.daylight ? 65535 : 0 obj.state.dark = !obj.state.daylight } obj.config.reachable = obj.config.configured } else { this.log.warn( '%s: %s: warning: ignoring unknown %s sensor %j', this.bridge.name, this.resource, this.obj.type, this.obj ) } break case 'CLIPGenericFlag': // 2.8 this.service = new Service.Switch(this.name, this.subtype) this.serviceList.push(this.service) this.type = { Characteristic: Characteristic.On, key: 'flag', name: 'on', unit: '', homekitValue: (v) => { return v }, bridgeValue: (v) => { return v }, setter: true } // Note that Eve handles a read-only switch correctly, but Home doesn't. if ( this.obj.manufacturername === 'homebridge-hue' && this.obj.modelid === 'CLIPGenericFlag' && this.obj.swversion === '0' ) { this.type.props = { perms: [Characteristic.Perms.PAIRED_READ, Characteristic.Perms.NOTIFY] } } break case 'CLIPGenericStatus': // 2.9 if ( this.obj.manufacturername === 'Philips' && this.obj.modelid === 'HUELABSVTOGGLE' && this.obj.swversion === '2.0' ) { // Hue labs toggle, see #1028. this.service = new Service.Switch(this.name, this.subtype) this.serviceList.push(this.service) this.type = { Characteristic: Characteristic.On, key: 'status', name: 'on', unit: '', homekitValue: (v) => { return v !== 0 }, bridgeValue: (v) => { return v ? 1 : 0 }, setter: true } break } this.service = new my.Services.Status(this.name, this.subtype) this.serviceList.push(this.service) this.type = { Characteristic: my.Characteristics.Status, key: 'status', name: 'status', unit: '', homekitValue: (v) => { return v > 127 ? 127 : v < -127 ? -127 : v }, bridgeValue: (v) => { return v }, setter: true } if ( this.obj.manufacturername === 'homebridge-hue' && this.obj.modelid === 'CLIPGenericStatus' ) { const min = parseInt(obj.swversion.split(',')[0]) const max = parseInt(obj.swversion.split(',')[1]) const step = parseInt(obj.swversion.split(',')[2]) // Eve 3.1 displays the following controls, depending on the properties: // 1. {minValue: 0, maxValue: 1, minStep: 1} switch // 2. {minValue: a, maxValue: b, minStep: 1}, 1 < b - a <= 20 down|up // 3. {minValue: a, maxValue: b}, (a, b) != (0, 1) slider // 4. {minValue: a, maxValue: b, minStep: 1}, b - a > 20 slider // Avoid the following bugs: // 5. {minValue: 0, maxValue: 1} nothing // 6. {minValue: a, maxValue: b, minStep: 1}, b - a = 1 switch* // *) switch sends values 0 and 1 instead of a and b; if (min === 0 && max === 0) { this.type.props = { perms: [Characteristic.Perms.PAIRED_READ, Characteristic.Perms.NOTIFY] } } else if (min >= -127 && max <= 127 && min < max) { if (min === 0 && max === 1) { // Workaround Eve bug (case 5 above). this.type.props = { minValue: min, maxValue: max, minStep: 1 } } else if (max - min === 1) { // Workaround Eve bug (case 6 above). this.type.props = { minValue: min, maxValue: max } } else if (step !== 1) { // Default to slider for backwards compatibility. this.type.props = { minValue: min, maxValue: max } } else { this.type.props = { minValue: min, maxValue: max, minStep: 1 } } } this.log.debug( '%s: %s: props: %j', this.bridge.name, this.resource, this.type.props ) } break default: this.log.warn( '%s: %s: warning: ignoring unknown sensor type %j', this.bridge.name, this.resource, this.obj ) break } if (this.service) { if (this.type.Characteristic) { const char = this.service.getCharacteristic(this.type.Characteristic) if (this.type.props) { char.setProps(this.type.props) } if (this.type.setter) { char.on('set', this.setValue.bind(this)) } if (this.type.history != null) { this.historyService = this.accessory .getHistoryService(this.type.history, this) this.history = this.accessory.history if (this.type.history !== this.history.type) { // History service already used for other type. this.historyService = null this.history = null this.type.history = null } const now = Math.round(new Date().valueOf() / 1000) const epoch = Math.round( new Date('2001-01-01T00:00:00Z').valueOf() / 1000 ) switch (this.type.history) { case 'door': this.hk.timesOpened = 0 this.historyService .addOptionalCharacteristic(eve.Characteristics.ResetTotal) this.historyService.getCharacteristic(eve.Characteristics.ResetTotal) .setValue(now - epoch) .on('set', (value, callback) => { this.hk.timesOpened = 0 this.service.updateCharacteristic( eve.Characteristics.TimesOpened, this.hk.timesOpened ) callback(null) }) // falls through case 'motion': this.history.entry.status = 0 break case 'weather': this.history.entry.temp = 0 this.history.entry.humidity = 0 this.history.entry.pressure = 0 break default: break } } this.checkValue(this.obj.state[this.type.key]) } this.service.addOptionalCharacteristic(my.Characteristics.LastUpdated) this.checkLastupdated(this.obj.state.lastupdated) if (this.obj.state.dark !== undefined) { this.service.addOptionalCharacteristic(my.Characteristics.Dark) this.checkDark(this.obj.state.dark) } if (this.obj.state.daylight !== undefined) { this.service.addOptionalCharacteristic(my.Characteristics.Daylight) this.checkDaylight(this.obj.state.daylight) } if (this.obj.state.tampered !== undefined && this.type.history !== 'door') { this.service.addOptionalCharacteristic(Characteristic.StatusTampered) this.checkTampered(this.obj.state.tampered) } if (this.obj.state.on !== undefined) { this.checkStateOn(this.obj.state.on) } if ( this.obj.state.daylight !== undefined && this.obj.state.status !== undefined ) { this.service.addOptionalCharacteristic(my.Characteristics.Status) this.service.getCharacteristic(my.Characteristics.Status) .setProps({ minValue: 100, maxValue: 230, perms: [Characteristic.Perms.PAIRED_READ, Characteristic.Perms.NOTIFY] }) this.service.addOptionalCharacteristic(my.Characteristics.LastEvent) this.service.addOptionalCharacteristic(my.Characteristics.Period) this.checkStatus(this.obj.state.status) } if (this.obj.config[this.type.durationKey] !== undefined) { this.checkDuration(this.obj.config[this.type.durationKey]) this.service.getCharacteristic(eve.Characteristics.Duration) .on('set', this.setDuration.bind(this)) delete this.duration } else if (this.duration !== undefined) { // Add fake duration for Hue motion sensor connected to the Hue bridge this.hk.duration = 5 this.service.getCharacteristic(eve.Characteristics.Duration) .setValue(this.hk.duration) .on('set', this.setDuration.bind(this)) } if (this.obj.config.sensitivity !== undefined) { this.checkSensitivity(this.obj.config.sensitivity) if (this.type.sensitivitymax != null) { this.service.getCharacteristic(eve.Characteristics.Sensitivity) .on('set', this.setSensitivity.bind(this)) } } if (this.type.key === 'temperature' && this.obj.config.offset !== undefined) { this.service.addOptionalCharacteristic(my.Characteristics.Offset) this.checkOffset(this.obj.config.offset) this.service.getCharacteristic(my.Characteristics.Offset) .on('set', this.setOffset.bind(this)) } if (this.obj.config.heatsetpoint !== undefined) { this.service.getCharacteristic(Characteristic.CurrentHeatingCoolingState) .setProps({ validValues: [ Characteristic.CurrentHeatingCoolingState.OFF, Characteristic.CurrentHeatingCoolingState.HEAT ] }) this.service.getCharacteristic(Characteristic.TargetHeatingCoolingState) .setProps({ validValues: [ Characteristic.TargetHeatingCoolingState.OFF, Characteristic.TargetHeatingCoolingState.HEAT ] }) .on('set', this.setTargetHeatingCoolingState.bind(this)) this.checkMode(this.obj.config.mode) if (this.obj.config.schedule_on !== undefined) { this.checkScheduleOn(this.obj.config.schedule_on) } this.service.getCharacteristic(Characteristic.TargetTemperature) .setProps({ minValue: 5, maxValue: 30, minStep: 0.5 }) .on('set', this.setTargetTemperature.bind(this)) this.checkHeatSetPoint(this.obj.config.heatsetpoint) this.service.addOptionalCharacteristic(eve.Characteristics.ProgramCommand) this.service.getCharacteristic(eve.Characteristics.ProgramCommand) .on('set', this.setProgramCommand.bind(this)) this.service.addOptionalCharacteristic(eve.Characteristics.ProgramData) this.service.getCharacteristic(eve.Characteristics.ProgramData) // .setValue(Buffer.from('ff04f6', 'hex').toString('base64')) .on('get', this.getProgramData.bind(this)) } if (this.obj.config.displayflipped !== undefined) { this.service.addOptionalCharacteristic(Characteristic.ImageMirroring) this.checkDisplayFlipped(this.obj.config.displayflipped) this.service.getCharacteristic(Characteristic.ImageMirroring) .on('set', this.setMirroring.bind(this)) } if (this.obj.config.locked !== undefined) { this.service.addOptionalCharacteristic(Characteristic.LockPhysicalControls) this.checkLocked(this.obj.config.locked) this.service.getCharacteristic(Characteristic.LockPhysicalControls) .on('set', this.setLocked.bind(this)) } this.service.addOptionalCharacteristic(Characteristic.StatusFault) this.checkReachable(this.obj.config.reachable) this.service.addOptionalCharacteristic(Characteristic.StatusActive) this.service.addOptionalCharacteristic(my.Characteristics.Enabled) this.checkOn(this.obj.config.on) this.service.getCharacteristic(my.Characteristics.Enabled) .on('set', this.setEnabled.bind(this)) if ( this.bridge.platform.config.resource && !this.service.testCharacteristic(my.Characteristics.Resource) ) { this.service.addOptionalCharacteristic(my.Characteristics.Resource) this.service.getCharacteristic(my.Characteristics.Resource) .updateValue(this.resource) } if ( this.bridge.platform.config.configuredName && !this.service.testCharacteristic(Characteristic.ConfiguredName) ) { this.service.addCharacteristic(Characteristic.ConfiguredName) // this.service.addOptionalCharacteristic(Characteristic.ConfiguredName) // this.service.getCharacteristic(Characteristic.ConfiguredName) // .on('set', this.setName.bind(this)) } } if (this.obj.config.battery !== undefined) { this.batteryService = this.accessory.getBatteryService( this.obj.config.battery ) } } createLabel (labelNamespace) { if (this.accessory.labelService == null) { this.service = new Service.ServiceLabel(this.name) this.service.getCharacteristic(Characteristic.ServiceLabelNamespace) .updateValue(labelNamespace) this.accessory.labelService = this.service } else { this.service = this.accessory.labelService // this.noSetNameCallback = true } } createButton (buttonIndex, buttonName, props) { // FIXME: subtype should be based on buttonIndex, not on buttonName. const service = new Service.StatelessProgrammableSwitch( this.name + ' ' + buttonName, buttonName ) this.serviceList.push(service) this.buttonMap['' + buttonIndex] = service service.getCharacteristic(Characteristic.ProgrammableSwitchEvent) .setProps(props) service.getCharacteristic(Characteristic.ServiceLabelIndex) .setValue(buttonIndex) } // ===== Bridge Events ========================================================= heartbeat (beat, obj) { // this.checkName(obj.name) if ( obj.state.daylight != null && obj.state.lightlevel == null && obj.state.status == null ) { // Daylight sensor on Hue bridge. obj.state.lightlevel = obj.state.daylight ? 65535 : 0 obj.state.dark = !obj.state.daylight } this.checkState(obj.state, false) if (obj.config.configured != null && obj.config.reachable == null) { obj.config.reachable = obj.config.configured } this.checkConfig(obj.config, false) } checkAttr (attr, event) { for (const key in attr) { switch (key) { case 'lastannounced': break case 'lastseen': // this.checkLastSeen(attr.lastseen) break // case 'name': // this.checkName(attr.name) // break default: break } } } checkState (state, event) { for (const key in state) { switch (key) { case 'buttonevent': if (event || this.bridge.eventStream == null) { this.checkButtonevent(state.buttonevent, state.lastupdated) } break case 'dark': this.checkDark(state.dark) break case 'daylight': this.checkDaylight(state.daylight) break case 'expectedeventduration': break case 'expectedrotation': if (event || this.bridge.eventStream == null) { this.checkButtonevent(state.expectedrotation, state.lastupdated) } break case 'lastupdated': this.checkLastupdated(state.lastupdated) break case 'rotaryevent': break default: if (key === this.type.key) { this.checkValue(state[this.type.key]) } else { this.log.debug( '%s: ignore unknown attribute state.%s', this.name, key ) } break } } } checkValue (value) { if (value === undefined) { return } if (this.obj.state[this.type.key] !== value) { this.log.debug( '%s: sensor %s changed from %j to %j', this.name, this.type.key, this.obj.state[this.type.key], value ) this.obj.state[this.type.key] = value } const hkValue = this.type.homekitValue(this.obj.state[this.type.key]) if (this.durationTimer != null) { if (hkValue !== 0) { clearTimeout(this.durationTimer) this.durationTimer = null this.log.debug( '%s: cancel timer to keep homekit %s on %s%s for %ss', this.name, this.type.name, hkValue, this.type.unit, this.hk.duration ) } return } if (this.hk[this.type.key] !== hkValue) { if (this.duration > 0 && hkValue === 0) { this.log.debug( '%s: keep homekit %s on %s%s for %ss', this.name, this.type.name, this.hk[this.type.key], this.type.unit, this.hk.duration ) const saved = { oldValue: this.hk[this.type.key], value: hkValue, duration: this.hk.duration } this.durationTimer = setTimeout(() => { this.log.info( '%s: set homekit %s from %s%s to %s%s, after %ss', this.name, this.type.name, saved.oldValue, this.type.unit, saved.value, this.type.unit, saved.duration ) this.durationTimer = null this.hk[this.type.key] = saved.value this.service .updateCharacteristic(this.type.Characteristic, this.hk[this.type.key]) this.addEntry(true) }, this.duration * 1000) return } if (this.hk[this.type.key] !== undefined) { this.log.info( '%s: set homekit %s from %s%s to %s%s', this.name, this.type.name, this.hk[this.type.key], this.type.unit, hkValue, this.type.unit ) } this.hk[this.type.key] = hkValue this.service .updateCharacteristic(this.type.Characteristic, this.hk[this.type.key]) this.addEntry(true) if ( this.type.key === 'power' && this.accessory.resource.config != null && this.accessory.resource.config.outlet ) { const hkInUse = hkValue > 0 ? 1 : 0 if (this.hk.inUse !== hkInUse) { if (this.hk.inUse !== undefined) { this.log.info( '%s: set homekit outlet in use from %s to %s', this.name, this.hk.inUse, hkInUse ) } this.hk.inUse = hkInUse this.service.getCharacteristic(Characteristic.OutletInUse) .updateValue(this.hk.inUse) } } } } addEntry (changed) { if (this.history == null) { return } const initialising = this.history.entry.time == null const now = Math.round(new Date().valueOf() / 1000) this.history.entry.time = now switch (this.history.type) { case 'door': if (changed) { this.hk.timesOpened += this.hk[this.type.key] this.service.updateCharacteristic( eve.Characteristics.TimesOpened, this.hk.timesOpened ) } // falls through case 'motion': if (changed) { this.hk.lastActivation = now - this.historyService.getInitialTime() this.service.updateCharacteristic( eve.Characteristics.LastActivation, this.hk.lastActivation ) } this.history.entry.status = this.hk[this.type.key] break case 'weather': { const key = this.type.key === 'temperature' ? 'temp' : this.type.key this.history.entry[key] = this.hk[this.type.key] if (changed || this.type.key !== this.history.resource.type.key) { return } } break default: return } if (initialising) { return } setTimeout(() => { // Make sure all weather entry attributes have been updated const entry = Object.assign({}, this.history.entry) this.log.debug('%s: add history entry %j', this.name, entry) this.historyService.addEntry(entry) }, 0) } checkButtonevent (rawEvent, lastupdated) { const event = this.convertButtonEvent?.(rawEvent) ?? rawEvent const previousEvent = this.convertButtonEvent?.(this.obj.state[this.type.key]) ?? this.obj.state[this.type.key] if ( rawEvent !== this.obj.state[this.type.key] || lastupdated > this.obj.state.lastupdated ) { this.log.debug( '%s: sensor %s %j on %s', this.name, this.type.key, rawEvent, lastupdated ) this.obj.state[this.type.key] = rawEvent } if (event !== previousEvent || lastupdated > this.obj.state.lastupdated) { const buttonIndex = this.type.homekitValue(event) const action = this.type.homekitAction( event, previousEvent, this.repeat != null && this.repeat.includes(buttonIndex) ) if (buttonIndex != null && action != null && this.buttonMap[buttonIndex] != null) { const char = this.buttonMap[buttonIndex] .getCharacteristic(Characteristic.ProgrammableSwitchEvent) if (char.props.validValues.includes(action)) { this.log.info( '%s: homekit button %s', this.buttonMap[buttonIndex].displayName, { 0: 'single press', 1: 'double press', 2: 'long press' }[action] ) char.updateValue(action) } } } } checkDark (dark) { if (this.obj.state.dark !== dark) { this.log.debug( '%s: sensor dark changed from %j to %j', this.name, this.obj.state.dark, dark ) this.obj.state.dark = dark } const hkDark = this.obj.state.dark ? 1 : 0 if (this.hk.dark !== hkDark) { if (this.hk.dark !== undefined) { this.log.info( '%s: set homekit dark from %s to %s', this.name, this.hk.dark, hkDark ) } this.hk.dark = hkDark this.service .updateCharacteristic(my.Characteristics.Dark, this.hk.dark) } } checkDaylight (daylight) { if (this.obj.state.daylight !== daylight) { this.log.debug( '%s: sensor daylight changed from %j to %j', this.name, this.obj.state.daylight, daylight ) this.obj.state.daylight = daylight } const hkDaylight = this.obj.state.daylight ? 1 : 0 if (this.hk.daylight !== hkDaylight) { if (this.hk.daylight !== undefined) { this.log.info( '%s: set homekit daylight from %s to %s', this.name, this.hk.daylight, hkDaylight ) } this.hk.daylight = hkDaylight this.service .updateCharacteristic(my.Characteristics.Daylight, this.hk.daylight) } } checkLastupdated (lastupdated) { if (this.obj.state.lastupdated < lastupdated) { this.log.debug( '%s: sensor lastupdated changed from %s to %s', this.name, this.obj.state.lastupdated, lastupdated ) this.obj.state.lastupdated = lastupdated } const hkLastupdated = dateToString(this.obj.state.lastupdated) if (this.hk.lastupdated !== hkLastupdated) { // this.log.info( // '%s: set homekit last updated from %s to %s', this.name, // this.hk.lastupdated, hkLastupdated // ) this.hk.lastupdated = hkLastupdated this.service .updateCharacteristic(my.Characteristics.LastUpdated, this.hk.lastupdated) } } checkStatus (status) { if (this.obj.state.status !== status) { this.log.debug( '%s: sensor status changed from %j to %j', this.name, this.obj.state.status, status ) this.obj.state.status = status } const hkStatus = this.obj.state.status if (this.hk.status !== hkStatus) { if (this.hk.status !== undefined) { this.log.info( '%s: set homekit status from %s to %s', this.name, this.hk.status, hkStatus ) } this.hk.status = hkStatus this.service .updateCharacteristic(my.Characteristics.Status, this.hk.status) } } checkConfig (config) { for (const key in config) { switch (key) { case 'alert': break case 'battery': this.accessory.checkBattery(config.battery) break case 'configured': break case 'devicemode': if (config.devicemode !== this.obj.config.devicemode) { this.log.warn( '%s: restart homebridge to handle new devicemode %s', this.name, config.devicemode ) this.obj.config.devicemode = config.devicemode } break case 'devicemodevalues': break case 'ledindication': break case 'mode': this.checkMode(config.mode) break case 'offset': this.checkOffset(config.offset) break case 'on': this.checkOn(config.on) break case 'pending': break case 'reachable': this.checkReachable(config.reachable) break case 'sensitivity': this.checkSensitivity(config.sensitivity) break case 'sensitivitymax': break case 'sunriseoffset': break case 'sunsetoffset': break case 'temperature': break case 'tholddark': break case 'tholdoffset': break case 'usertest': break default: this.log.debug( '%s: ignore unknown attribute config.%s', this.name, key ) break } } } // checkName (name) { // if (this.obj.name !== name) { // this.log.debug( // '%s: name changed from %j to %j', this.name, this.obj.name, name // ) // this.obj.name = name // } // const hkName = this.obj.name // if (this.hk.name !== hkName) { // if (this.hk.name !== undefined) { // this.log.info( // '%s: set homekit name from %j to %j', this.name, this.hk.name, hkName // ) // } // this.hk.name = hkName // this.service.getCharacteristic(Characteristic.ConfiguredName) // .updateValue(hkName) // this.name = this.hk.name // } // } checkOn (on) { if (this.obj.config.on !== on) { this.log.debug( '%s: sensor on changed from %j to %j', this.name, this.obj.config.on, on ) this.obj.config.on = on } const hkEnabled = this.obj.config.on if (this.hk.enabled !== hkEnabled) { if (this.hk.enabled !== undefined) { this.log.info( '%s: set homekit enabled from %s to %s', this.name, this.hk.enabled, hkEnabled ) } this.hk.enabled = hkEnabled this.service .updateCharacteristic(Characteristic.StatusActive, this.hk.enabled) .updateCharacteristic(my.Characteristics.Enabled, this.hk.enabled) } } checkReachable (reachable) { if (this.obj.config.reachable !== reachable) { this.log.debug( '%s: sensor reachable changed from %j to %j', this.name, this.obj.config.reachable, reachable ) this.obj.config.reachable = reachable } const hkFault = this.obj.config.reachable === false ? 1 : 0 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) } } checkSensitivity (sensitivity) { if (this.obj.config.sensitivity == null) { return } if (this.obj.config.sensitivity !== sensitivity) { this.log.debug( '%s: sensor sensitivity changed from %j to %j', this.name, this.obj.config.sensitivity, sensitivity ) this.obj.config.sensitivity = sensitivity } const hkSensitivity = sensitivity === this.type.sensitivitymax ? 0 : sensitivity === 0 ? 7 : 4 if (this.hk.sensitivity !== hkSensitivity) { if (this.hk.sensitivity !== undefined) { this.log.info( '%s: set homekit sensitivity from %s to %s', this.name, this.hk.sensitivity, hkSensitivity ) } this.hk.sensitivity = hkSensitivity this.service.updateCharacteristic( eve.Characteristics.Sensitivity, this.hk.sensitivity ) } } // ===== Homekit Events ======================================================== identify (callback) { if (this.obj.config.alert === undefined) { return callback() } this.log.info('%s: identify', this.name) this.put('/config', { alert: 'select' }).then((obj) => { return callback() }).catch((error) => { return callback(error) }) } setValue (value, callback) { if (typeof value === 'number') { value = Math.round(value) } if (value === this.hk[this.type.key]) { return callback() } this.log.info( '%s: homekit %s changed from %s%s to %s%s', this.name, this.type.name, this.hk[this.type.key], this.type.unit, value, this.type.unit ) this.hk[this.type.key] = value const newValue = this.type.bridgeValue(value) const body = {} body[this.type.key] = newValue this.put('/state', body).then((obj) => { this.obj.state[this.type.key] = newValue this.value = newValue return callback() }).catch((error) => { return 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 const hueDuration = duration === 5 ? 0 : duration if (this.duration !== undefined) { this.duration = hueDuration return callback() } const body = {} body[this.type.durationKey] = hueDuration this.put('/config', body).then((obj) => { this.obj.config[this.type.durationKey] = hueDuration return callback() }).catch((error) => { return callback(error) }) } setEnabled (enabled, callback) { if (enabled === this.hk.enabled) { return callback() } this.log.info( '%s: homekit enabled changed from %s to %s', this.name, this.hk.enabled, enabled ) this.hk.enabled = enabled const on = this.hk.enabled this.put('/config', { on }).then((obj) => { this.obj.config.on = on this.service .updateCharacteristic(Characteristic.StatusActive, this.hk.enabled) return callback() }).catch((error) => { return callback(error) }) } // setName (name, callback) { // if (this.noSetNameCallback) { // callback = () => {} // } // if (name === this.hk.name) { // return callback() // } // name = name.trim() // .slice(0, 32).trim() // if (name === '') { // return callback(new Error()) // } // this.log.info( // '%s: homekit name changed from %j to %j', this.name, this.hk.name, name // ) // this.put('', { name: name }).then((obj) => { // if (obj.name == null) { // this.obj.name = name // this.hk.name = name // return callback(new Error()) // } // this.obj.name = obj.name // this.name = obj.name // setImmediate(() => { // this.hk.name = name // this.service.getCharacteristic(Characteristic.ConfiguredName) // .updateValue(this.hk.name) // }) // return callback() // }).catch((error) => { // return callback(error) // }) // } setSensitivity (sensitivity, callback) { if (sensitivity === this.hk.sensitivity) { return callback() } this.log.info( '%s: homekit sensitivity changed from %s to %s', this.name, this.hk.sensitivity, sensitivity ) this.hk.sensitivity = sensitivity const hueSensitivity = this.hk.sensitivity === 0 ? this.type.sensitivitymax : this.hk.sensitivity === 7 ? 0 : Math.round(this.type.sensitivitymax / 2) this.put('/config', { sensitivity: hueSensitivity }).then((obj) => { this.obj.config.sensitivity = hueSensitivity return callback() }).catch((error) => { return callback(error) }) } put (resource, body) { return this.bridge.put(this.resource + resource, body) } } export { HueSensor }