@hoobs/hue
Version:
HOOBS plugin for Philips Hue and deCONZ
461 lines (429 loc) • 15.2 kB
JavaScript
// homebridge-hue/lib/HueAccessory.js
// Copyright © 2016-2020 Erik Baauw. All rights reserved.
//
// Homebridge plugin for Philips Hue and/or deCONZ.
//
// HueAccessory provides support for ZigBee devices.
'use strict'
const fakegatoHistory = require('fakegato-history')
const homebridgeLib = require('homebridge-lib')
const HueLightModule = require('./HueLight')
const HueSensorModule = require('./HueSensor')
const HueLight = HueLightModule.HueLight
const HueSensor = HueSensorModule.HueSensor
// Link this module to HuePlatform.
module.exports = {
setHomebridge: setHomebridge,
HueAccessory: HueAccessory
}
function toInt (value, minValue, maxValue) {
const n = parseInt(value)
if (isNaN(n) || n < minValue) {
return minValue
}
if (n > maxValue) {
return maxValue
}
return n
}
const formatError = homebridgeLib.CommandLineTool.formatError
// ===== Homebridge ============================================================
// Link this module to homebridge.
let Service
let Characteristic
let my
let eve
let HistoryService
function setHomebridge (homebridge, _my, _eve) {
Service = homebridge.hap.Service
Characteristic = homebridge.hap.Characteristic
my = _my
eve = _eve
HistoryService = fakegatoHistory(homebridge)
HueLightModule.setHomebridge(homebridge, _my, _eve)
HueSensorModule.setHomebridge(homebridge, _my, _eve)
}
// ===== HueAccessory ==========================================================
function HueAccessory (bridge, serialNumber, isMulti = false) {
this.log = bridge.log
this.bridge = bridge
this.serialNumber = serialNumber
// jshint -W106
this.uuid_base = this.serialNumber
// jshint +W106
this.isMulti = isMulti
this.resources = {
sensors: { other: [] },
lights: { other: [] }
}
this.sensors = {}
this.lights = {}
this.groups = {}
this.serviceList = []
this.state = {}
this.hk = {}
}
// ===== Resources =============================================================
HueAccessory.prototype.addGroupResource = function (id, obj) {
if (this.resources.group == null) {
this.resources.group = { id: id, obj: obj }
}
}
HueAccessory.prototype.addLightResource = function (id, obj) {
switch (obj.type) {
case 'Extended color light':
case 'Color light':
if (!this.isMulti && this.resources.lights.main == null) {
this.resources.lights.main = { id: id, obj: obj }
return
}
// falls through
default:
this.resources.lights.other.push({ id: id, obj: obj })
break
}
}
HueAccessory.prototype.addSensorResource = function (id, obj) {
const type = obj.type.substring(obj.type[0] === 'Z' ? 3 : 4)
switch (type) {
case 'OpenClose':
case 'Presence':
case 'LightLevel':
case 'Temperature':
case 'Humidity':
case 'Pressure':
case 'Consumption':
case 'Power':
if (!this.isMulti && this.resources.sensors[type] == null) {
this.resources.sensors[type] = { id: id, obj: obj }
return
}
// falls through
default:
this.resources.sensors.other.push({ id: id, obj: obj })
break
}
}
HueAccessory.prototype.expose = function () {
this.exposeGroupResources()
this.exposeLightResources()
this.exposeSensorResources()
return [this]
}
HueAccessory.prototype.exposeGroupResources = function () {
if (this.resources.group != null) {
const id = this.resources.group.id
const obj = this.resources.group.obj
this.name = obj.name
this.manufacturer = this.bridge.manufacturer
this.model = obj.type
this.version = this.bridge.version
this.log.debug(
'%s: %s: %s "%s"', this.bridge.name,
this.serialNumber, this.model, this.name
)
this.exposeGroupResource(id, obj)
}
}
HueAccessory.prototype.exposeGroupResource = function (id, obj) {
this.log.debug(
'%s: /groups/%d: %s "%s"', this.bridge.name, id, obj.type, obj.name
)
try {
const group = new HueLight(this, id, obj, 'group')
this.bridge.groups[id] = group
this.groups[id] = group
this.serviceList.push(group.service)
this.resource = this.resource || group
this.service = this.service || group.service
if (this.bridge.platform.config.scenes) {
const SceneService = this.bridge.platform.config.scenesAsSwitch
? Service.Switch
: my.Services.Resource
const SceneCharacteristic = this.bridge.platform.config.scenesAsSwitch
? Characteristic.On
: my.Characteristics.Enabled
if (obj.scenes != null) {
for (const scene of obj.scenes) {
const resource = this.bridge.isHue
? '/scenes/' + scene.id
: '/groups/' + id + '/scenes/' + scene.id
const sceneName = obj.name + ' ' + scene.name
const path = this.bridge.isHue
? '/groups/' + id + '/action'
: resource + '/recall'
const body = this.bridge.isHue
? { scene: scene.id }
: undefined
this.log.debug('%s: %s: "%s"', this.bridge.name, resource, sceneName)
const service = new SceneService(sceneName, 'scene' + scene.id)
service.getCharacteristic(SceneCharacteristic)
.setValue(0)
.on('set', (value, callback) => {
if (!value) {
return callback()
}
this.log('%s: recall scene %j', this.bridge.name, sceneName)
this.bridge.request('put', path, body).then((obj) => {
setTimeout(() => {
service.updateCharacteristic(SceneCharacteristic, 0)
}, this.bridge.platform.config.resetTimeout)
callback()
})
})
if (this.bridge.platform.config.resource) {
service.addOptionalCharacteristic(my.Characteristics.Resource)
service.getCharacteristic(my.Characteristics.Resource)
.updateValue(resource)
}
this.serviceList.push(service)
}
}
}
} catch (e) {
this.log.error(
'%s: error: /groups/%d: %j\n%s', this.bridge.name, id, obj, formatError(e)
)
}
}
HueAccessory.prototype.exposeLightResources = function () {
if (this.resources.lights.main != null) {
this.resources.lights.other.unshift(this.resources.lights.main)
}
if (this.resources.lights.other.length > 0) {
const obj = this.resources.lights.other[0].obj
if (this.service == null) {
this.name = obj.name
this.manufacturer = this.isMulti
? this.bridge.manufacturer : obj.manufacturername
this.model = this.isMulti ? 'MultiLight' : obj.modelid
this.version = this.isMulti ? this.bridge.version : obj.swversion
this.log.debug(
'%s: %s: %s %s "%s"', this.bridge.name, this.serialNumber,
this.manufacturer, this.model, this.name
)
}
for (const resource of this.resources.lights.other) {
this.exposeLightResource(resource.id, resource.obj)
}
}
}
HueAccessory.prototype.exposeLightResource = function (id, obj) {
this.log.debug(
'%s: /lights/%d: %s "%s"', this.bridge.name, id, obj.type, obj.name
)
try {
const light = new HueLight(this, id, obj)
this.bridge.lights[id] = light
this.lights[id] = light
this.serviceList.push(light.service)
this.resource = this.resource || light
this.service = this.service || light.service
this.lightService = this.lightService || light.service
} catch (e) {
this.log.error(
'%s: error: /lights/%d: %j\n%s', this.bridge.name, id, obj, formatError(e)
)
}
}
HueAccessory.prototype.exposeSensorResources = function () {
// Force the order of processing the sensor resources.
if (this.resources.sensors.Power != null) {
this.resources.sensors.other.unshift(this.resources.sensors.Power)
}
if (this.resources.sensors.Consumption != null) {
this.resources.sensors.other.unshift(this.resources.sensors.Consumption)
}
if (this.resources.sensors.Pressure != null) {
this.resources.sensors.other.unshift(this.resources.sensors.Pressure)
}
if (this.resources.sensors.Humidity != null) {
this.resources.sensors.other.unshift(this.resources.sensors.Humidity)
}
if (this.resources.sensors.Temperature != null) {
this.resources.sensors.other.unshift(this.resources.sensors.Temperature)
}
if (this.resources.sensors.LightLevel != null) {
this.resources.sensors.other.unshift(this.resources.sensors.LightLevel)
}
if (this.resources.sensors.Presence != null) {
this.resources.sensors.other.unshift(this.resources.sensors.Presence)
}
if (this.resources.sensors.OpenClose != null) {
this.resources.sensors.other.unshift(this.resources.sensors.OpenClose)
}
if (this.resources.sensors.other.length > 0) {
const obj = this.resources.sensors.other[0].obj
if (this.service == null) {
this.name = obj.name
this.manufacturer = this.isMulti
? this.bridge.manufacturer : obj.manufacturername
this.model = this.isMulti ? 'MultiCLIP' : obj.modelid
this.version = this.isMulti ? this.bridge.version : obj.swversion
this.log.debug(
'%s: %s: %s %s "%s"', this.bridge.name, this.serialNumber,
this.manufacturer, this.model, this.name
)
}
if (
this.bridge.platform.config.forceEveWeather &&
this.resources.sensors.other.length === 2 &&
this.resources.sensors.other[0].obj.type.slice(-11) === 'Temperature' &&
this.resources.sensors.other[1].obj.type.slice(-8) === 'Humidity'
) {
this.log.debug(
'%s: /sensors/0: CLIPPressure "Dummy Pressure"', this.bridge.name
)
const service = new eve.Services.AirPressureSensor('Dummy Pressure')
const perms = [Characteristic.Perms.READ, Characteristic.Perms.HIDDEN]
service.getCharacteristic(eve.Characteristics.AirPressure)
.setProps({ perms: perms })
.updateValue(1000)
service.getCharacteristic(eve.Characteristics.Elevation)
.setProps({ perms: perms })
.updateValue(0)
this.serviceList.push(service)
}
for (const resource of this.resources.sensors.other) {
this.exposeSensorResource(resource.id, resource.obj)
}
}
}
HueAccessory.prototype.exposeSensorResource = function (id, obj) {
this.log.debug(
'%s: /sensors/%d: %s "%s"', this.bridge.name, id, obj.type, obj.name
)
try {
const sensor = new HueSensor(this, id, obj)
if (sensor.service) {
this.bridge.sensors[id] = sensor
this.sensors[id] = sensor
for (const service of sensor.serviceList) {
if (service !== this.lightService) {
this.serviceList.push(service)
}
}
this.resource = this.resource || sensor
this.service = this.service || sensor.service
this.sensorService = this.sensorService || sensor.service
}
} catch (e) {
this.log.error(
'%s: error: /sensors/%d: %j\n%s', this.bridge.name, id, obj, formatError(e)
)
}
}
// ===== Services ==============================================================
HueAccessory.prototype.getServices = function () {
const serviceList = [this.infoService]
this.labelService && serviceList.push(this.labelService)
for (const service of this.serviceList) {
serviceList.push(service)
}
this.batteryService && serviceList.push(this.batteryService)
this.historyService && serviceList.push(this.historyService)
return serviceList
}
HueAccessory.prototype.getInfoService = function (obj) {
if (!this.infoService) {
this.infoService = new Service.AccessoryInformation()
this.infoService
.updateCharacteristic(Characteristic.Manufacturer, this.manufacturer)
.updateCharacteristic(Characteristic.Model, this.model)
.updateCharacteristic(Characteristic.SerialNumber, this.serialNumber)
.updateCharacteristic(Characteristic.FirmwareRevision, this.version)
}
return this.infoService
}
HueAccessory.prototype.getBatteryService = function (battery) {
if (this.batteryService == null) {
this.batteryService = new Service.BatteryService(this.name)
this.batteryService.getCharacteristic(Characteristic.ChargingState)
.setValue(Characteristic.ChargingState.NOT_CHARGEABLE)
this.state.battery = battery
this.checkBattery(battery)
}
return this.batteryService
}
HueAccessory.prototype.checkBattery = function (battery) {
if (this.state.battery !== battery) {
this.log.debug(
'%s: sensor battery changed from %j to %j', this.name,
this.state.battery, battery
)
this.state.battery = battery
}
const hkBattery = toInt(this.state.battery, 0, 100)
if (this.hk.battery !== hkBattery) {
if (this.hk.battery !== undefined) {
this.log.info(
'%s: set homekit battery level from %s%% to %s%%', this.name,
this.hk.battery, hkBattery
)
}
this.hk.battery = hkBattery
this.hk.lowBattery =
this.hk.battery <= this.bridge.platform.config.lowBattery
? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
: Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
this.batteryService
.updateCharacteristic(Characteristic.BatteryLevel, this.hk.battery)
.updateCharacteristic(
Characteristic.StatusLowBattery, this.hk.lowBattery
)
}
}
HueAccessory.prototype.getLabelService = function (labelNamespace) {
if (this.labelService == null) {
this.labelService = new Service.ServiceLabel(this.name)
this.labelService.getCharacteristic(Characteristic.ServiceLabelNamespace)
.updateValue(labelNamespace)
}
return this.labelService
}
HueAccessory.prototype.getHistoryService = function (type, resource) {
if (this.historyService == null) {
this.historyService = new HistoryService(type, { displayName: this.name }, {
disableTimer: true,
storage: 'fs',
path: this.bridge.platform.api.user.storagePath() + '/accessories',
filename: 'history_' + this.serialNumber + '.json'
})
this.history = { entry: {}, type: type, resource: resource }
}
return this.historyService
}
// ===== Services ==============================================================
HueAccessory.prototype.identify = function (callback) {
if (this.resource.type === 'group') {
this.log.info(
'%s: %s: %s "%s"', this.bridge.name, this.serialNumber,
this.model, this.name
)
this.log.info(
'%s: /groups/%d: %s "%s"', this.bridge.name, this.resource.id,
this.resource.obj.type, this.resource.name
)
} else {
this.log.info(
'%s: %s: %s %s "%s"', this.bridge.name, this.serialNumber,
this.manufacturer, this.model, this.name
)
}
for (const resource of this.resources.lights.other) {
const light = this.lights[resource.id]
this.log.info(
'%s: /lights/%d: %s "%s"', this.bridge.name, resource.id,
light.obj.type, light.name
)
}
for (const resource of this.resources.sensors.other) {
const sensor = this.sensors[resource.id]
this.log.info(
'%s: /sensors/%d: %s "%s"', this.bridge.name, resource.id,
sensor.obj.type, sensor.name
)
}
// TODO loop over all resources
this.resource.identify(callback)
}