@hoobs/hue
Version:
HOOBS plugin for Philips Hue and deCONZ
1,140 lines (1,086 loc) • 35.9 kB
JavaScript
// homebridge-hue/lib/HueBridge.js
// Copyright © 2016-2020 Erik Baauw. All rights reserved.
//
// Homebridge plugin for Philips Hue and/or deCONZ.
//
// HueBridge provides support for Philips Hue bridges and dresden elektronik
// deCONZ gateways.
//
// Todo:
// - Support rules in separate accessories.
'use strict'
const homebridgeLib = require('homebridge-lib')
const os = require('os')
const semver = require('semver')
const util = require('util')
const WebSocket = require('ws')
const HueAccessoryModule = require('./HueAccessory')
const HueScheduleModule = require('./HueSchedule')
const HueAccessory = HueAccessoryModule.HueAccessory
const HueClient = require('./HueClient')
const HueSchedule = HueScheduleModule.HueSchedule
module.exports = {
setHomebridge: setHomebridge,
HueBridge: HueBridge
}
const formatError = homebridgeLib.CommandLineTool.formatError
// ===== Homebridge ============================================================
let Service
let Characteristic
let my
function setHomebridge (homebridge, _my, _eve) {
HueAccessoryModule.setHomebridge(homebridge, _my, _eve)
HueScheduleModule.setHomebridge(homebridge, _my)
Service = homebridge.hap.Service
Characteristic = homebridge.hap.Characteristic
my = _my
}
// ===== HueBridge =============================================================
const repeaterTypes = [
'Range extender', // Trådfri repeater, XBee
'Configuration tool' // RaspBee, ConBee, ConBee II
]
function HueBridge (platform, host) {
this.log = platform.log
this.platform = platform
this.host = host
this.hostname = host.split(':')[0]
this.name = this.platform.maskHost(this.hostname)
this.type = 'bridge'
this.defaultTransitiontime = 0.4
this.state = {
heartrate: this.platform.config.heartrate,
transitiontime: this.defaultTransitiontime,
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 = {}
}
HueBridge.prototype.getServices = function () {
this.log.info('%s: %d services', this.name, this.serviceList.length)
return this.serviceList
}
HueBridge.prototype.accessories = async function () {
this.accessoryMap = {}
this.accessoryList = []
try {
const obj = await this.getConfig()
await this.exposeBridge(obj)
await this.createUser()
const state = await this.getFullState()
await this.exposeResources(state)
} catch (error) {
if (error.message !== 'unknown bridge') {
this.log.error('%s: %s', this.name, formatError(error))
}
}
this.log.info('%s: %d accessories', this.name, this.accessoryList.length)
return this.accessoryList
}
HueBridge.prototype.getConfig = async function () {
if (this.hueClient == null) {
this.hueClient = new HueClient({
host: this.host,
timeout: this.platform.config.timeout
})
}
try {
const obj = await this.hueClient.config()
delete this.hueClient
return obj
} catch (error) {
this.log.error('%s: %s - retrying in 15s', this.name, formatError(error))
await homebridgeLib.timeout(15000)
return this.getConfig()
}
}
HueBridge.prototype.getInfoService = function () {
return this.infoService
}
HueBridge.prototype.exposeBridge = async function (obj) {
this.name = obj.name
this.serialNumber = obj.bridgeid
// jshint -W106
this.uuid_base = this.serialNumber
// jshint +W106
this.username = this.platform.config.users[this.serialNumber] || ''
this.config = {
parallelRequests: 10,
nativeHomeKitLights: this.platform.config.nativeHomeKitLights,
nativeHomeKitSensors: this.platform.config.nativeHomeKitSensors
}
this.model = obj.modelid
if (
this.model === 'BSB002' && obj.bridgeid.substring(0, 6) !== '001788' &&
obj.bridgeid.substring(0, 6) !== 'ECB5FA'
) {
this.model = 'HA-Bridge'
}
if (this.model == null) {
this.model = 'Tasmota'
}
this.philips = 'Philips'
const recommendedVersion = this.platform.packageJson.engines[obj.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.manufacturer = this.philips
this.idString = util.format(
'%s: %s %s %s v%s, api v%s', this.name, this.manufacturer,
this.model, this.type, obj.swversion, obj.apiversion
)
this.log.info(this.idString)
this.version = obj.apiversion
if (!semver.satisfies(this.version, recommendedVersion)) {
this.log.debug(
'%s: warning: not using recommended Hue bridge api version %s',
this.name, recommendedVersion
)
}
if (semver.gte(this.version, '1.36.0')) {
this.philips = 'Signify Netherlands B.V.'
}
this.config.link = semver.lt(this.version, '1.31.0')
this.config.linkbutton = this.platform.config.linkbutton == null
? this.config.link
: this.platform.config.linkbutton
break
case 'deCONZ': // deCONZ rest api
if (obj.bridgeid === '0000000000000000') {
this.log.info(
'%s: RaspBee/ConBee not yet initialised - wait 1 minute', obj.name
)
await homebridgeLib.timeout(60000)
obj = await this.getConfig()
return this.exposeBridge(obj)
}
this.isDeconz = true
this.manufacturer = 'dresden elektronik'
this.type = 'gateway'
this.version = obj.swversion
this.config.nativeHomeKitLights = false
this.config.nativeHomeKitSensors = false
this.idString = util.format(
'%s: %s %s %s v%s, api v%s', this.name, this.manufacturer,
this.model, this.type, obj.swversion, obj.apiversion
)
this.log.info(this.idString)
if (!semver.satisfies(this.version, recommendedVersion)) {
this.log.debug(
'%s: warning: not using recommended deCONZ gateway version %s',
this.name, recommendedVersion
)
}
break
case 'HA-Bridge':
this.manufacturer = 'HA-Bridge'
this.idString = util.format(
'%s: %s v%s, api v%s', this.name, this.model,
obj.swversion, obj.apiversion
)
this.log.info(this.idString)
this.version = obj.apiversion
this.config.nativeHomeKitLights = false
this.config.nativeHomeKitSensors = false
break
case 'Tasmota':
this.manufacturer = 'Sonoff'
this.idString = util.format(
'%s: %s %s v%s, api v%s', this.name, this.manufacturer,
this.model, obj.swversion, obj.apiversion
)
this.version = obj.apiversion
this.config.nativeHomeKitLights = false
this.config.nativeHomeKitSensors = false
this.username = 'homebridgehue'
break
default:
this.log.debug(
'%s: warning: ignoring unknown bridge/gateway %j',
this.name, obj
)
throw new Error('unknown bridge')
}
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.obj = obj
this.service = new my.Services.HueBridge(this.name)
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()).substring(0, 24))
this.service.getCharacteristic(my.Characteristics.TransitionTime)
.updateValue(this.state.transitiontime)
.on('set', this.setTransitionTime.bind(this))
if (this.isHue || this.isDeconz) {
this.service.getCharacteristic(my.Characteristics.Restart)
.updateValue(false)
.on('set', this.setRestart.bind(this))
}
if (this.config.linkbutton) {
this.state.linkbutton = false
if (this.config.link) {
this.state.hkLink = 0
this.service.getCharacteristic(my.Characteristics.Link)
.updateValue(this.state.hkLink)
.on('set', this.setLink.bind(this))
}
this.switchService = new Service.StatelessProgrammableSwitch(this.name)
this.serviceList.push(this.switchService)
this.switchService
.getCharacteristic(Characteristic.ProgrammableSwitchEvent)
.setProps({
minValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
axValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS
})
}
this.accessoryList.push(this)
}
HueBridge.prototype.createUser = async function () {
if (this.username) {
return
}
try {
const devicetype = ('homebridge-hue#' + os.hostname().split('.')[0])
.slice(0, 40)
const obj = await this.request('post', '/', { devicetype: devicetype })
this.username = obj[0].success.username
let s = '\n'
s += ' "platforms": [\n'
s += ' {\n'
s += ' "platform": "Hue",\n'
s += ' "users": {\n'
s += ' "' + this.serialNumber + '": "' + this.username + '"\n'
s += ' }\n'
s += ' }\n'
s += ' ]'
this.log.info(
'%s: created user - please edit config.json and restart homebridge%s',
this.name, s
)
delete this.hueClient
return
} catch (error) {
const s = this.isDeconz
? 'unlock gateway'
: 'press link button on the bridge'
this.log.info('%s: %s to create a user - retrying in 15s', this.name, s)
await homebridgeLib.timeout(15000)
return this.createUser()
}
}
HueBridge.prototype.getFullState = async function () {
const state = await this.request('get', '/')
const group0 = await this.request('get', '/groups/0')
state.groups[0] = group0
if (state.resourcelinks == null) {
const resourcelinks = await this.request('get', '/resourcelinks')
state.resourcelinks = resourcelinks
}
this.fullState = state
return state
}
HueBridge.prototype.exposeResources = async function (obj) {
const whitelist = {
groups: {},
lights: {},
sensors: {},
schedules: {},
rules: {}
}
this.blacklist = {
groups: {},
lights: {},
sensors: {},
schedules: {},
rules: {}
}
this.multiclip = {}
this.multilight = {}
this.splitlight = {}
this.outlet = {
groups: {},
lights: {}
}
this.valve = {}
this.wallswitch = {}
this.obj = obj.config
for (const key in obj.resourcelinks) {
const link = obj.resourcelinks[key]
if (link.name === 'homebridge-hue' && link.links && link.description) {
const list = link.description.toLowerCase()
switch (list) {
case 'blacklist':
case 'lightlist':
case 'multiclip':
case 'multilight':
case 'outlet':
case 'splitlight':
case 'valve':
case 'wallswitch':
case 'whitelist':
break
default:
this.log.debug(
'%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 (!whitelist[type]) {
this.log.debug(
'%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
)
this.log.info(
'%s: gateway not yet initialised - wait 1 minute', this.name
)
await homebridgeLib.timeout(60000)
const state = await this.getFullState()
return this.exposeResources(state)
}
if (list === 'multiclip') {
if (
type !== 'sensors' || (
obj[type][id].type.substring(0, 4) !== 'CLIP' &&
obj[type][id].type !== 'Daylight'
)
) {
this.log.debug(
'%s: /resourcelinks/%d: %s: ignoring unsupported multiclip resource',
this.name, key, resource
)
continue
}
if (this.multiclip[id] != null) {
this.log.debug(
'%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.debug(
'%s: /resourcelinks/%d: %s: ignoring unsupported multilight resource',
this.name, key, resource
)
continue
}
if (this.multilight[id] != null) {
this.log.debug(
'%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.debug(
'%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.debug(
'%s: /resourcelinks/%d: %s: ignoring unsupported splitlight resource',
this.name, key, resource
)
continue
}
this.splitlight[id] = true
} else if (list === 'valve') {
if (type !== 'lights') {
this.log.debug(
'%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.debug(
'%s: /resourcelinks/%d: %s: ignoring unsupported wallswitch resource',
this.name, key, resource
)
continue
}
this.wallswitch[id] = true
} else if (list === 'whitelist') {
whitelist[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 && this.platform.config.scenes) {
for (const id in obj.groups) {
obj.groups[id].scenes = []
}
for (const key in obj.scenes) {
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 })
}
}
for (const id in whitelist.groups) {
this.exposeGroup(id, obj.groups[id])
}
this.exposeGroups(obj.groups)
for (const id in whitelist.lights) {
this.exposeLight(id, obj.lights[id])
}
this.exposeLights(obj.lights)
for (const id in whitelist.sensors) {
this.exposeSensor(id, obj.sensors[id])
}
this.exposeSensors(obj.sensors)
for (const id in whitelist.schedules) {
this.exposeSchedule(id, obj.schedules[id])
}
this.exposeSchedules(obj.schedules)
for (const id in whitelist.rules) {
this.exposeRule(id, obj.rules[id])
}
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.obj.websocketport) {
this.listen()
}
}
HueBridge.prototype.exposeSensors = function (sensors) {
if (this.platform.config.sensors) {
for (const id in sensors) {
const sensor = sensors[id]
if (this.sensors[id]) {
this.log.debug('%s: /sensors/%d: whitelisted', this.name, id)
} else 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.substring(0, 4) === 'CLIP' &&
this.platform.config.excludeSensorTypes.CLIP
)
) {
this.log.debug(
'%s: /sensors/%d: %s excluded', this.name, id, sensor.type
)
} 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)
}
}
}
}
HueBridge.prototype.exposeSensor = function (id, obj) {
obj.manufacturername = obj.manufacturername.replace(/\//g, '')
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 (
obj.manufacturername === this.philips &&
(obj.modelid === 'SML001' || obj.modelid === 'SML002')
) {
// Hue motion sensor.
if (obj.type === 'ZHATemperature' || obj.type === 'ZLLTemperature') {
serialNumber += '-T'
}
} else if (
obj.manufacturername === 'Samjin' && obj.modelid === 'multi'
) {
// Samsung SmartThings multupurpose sensor.
if (obj.type === 'ZHATemperature') {
serialNumber += '-T'
} else if (obj.type === 'ZHAVibration') {
serialNumber += '-V'
}
}
}
if (
obj.manufacturername === 'Develco Products AS' &&
(obj.modelid === 'SMSZB-120' || obj.modelid === 'HESZB-120')
) {
// Develco smoke sensor.
if (obj.type === 'ZHATemperature') {
serialNumber += '-T'
}
}
}
}
if (
obj.manufacturername === 'homebridge-hue' &&
obj.modelid === obj.type &&
obj.uniqueid.split('-')[1] === id
) {
// Combine multiple CLIP sensors into one accessory.
this.log.error(
'%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)
}
HueBridge.prototype.exposeLights = function (lights) {
if (this.platform.config.lights) {
for (const id in lights) {
const light = lights[id]
if (this.lights[id]) {
this.log.debug('%s: /lights/%d: whitelisted', this.name, id)
} else 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 if (
repeaterTypes.includes(light.type) ||
(light.type === 'Unknown' && light.manufacturername === 'dresden elektronik')
) {
this.log.debug('%s: /lights/%d: ignore repeater %j', this.name, id, light)
} else {
this.exposeLight(id, light)
}
}
}
}
HueBridge.prototype.exposeLight = function (id, obj) {
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)
}
HueBridge.prototype.exposeGroups = function (groups) {
if (this.platform.config.groups) {
for (const id in groups) {
const group = groups[id]
if (this.groups[id]) {
this.log.debug('%s: /groups/%d: whitelisted', this.name, id)
} else 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)
}
}
}
}
HueBridge.prototype.exposeGroup = function (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)
}
HueBridge.prototype.exposeSchedules = function (schedules) {
if (this.platform.config.schedules) {
for (const id in schedules) {
if (this.schedules[id]) {
this.log.debug('%s: /schedules/%d: whitelisted', this.name, id)
} else if (this.blacklist.schedules[id]) {
this.log.debug('%s: /schedules/%d: blacklisted', this.name, id)
} else {
this.exposeSchedule(id, schedules[id])
}
}
}
}
HueBridge.prototype.exposeSchedule = function (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)
)
}
}
HueBridge.prototype.exposeRules = function (rules) {
if (this.platform.config.rules) {
for (const id in rules) {
if (this.rules[id]) {
this.log.debug('%s: /rules/%d: whitelisted', this.name, id)
} else if (this.blacklist.rules[id]) {
this.log.debug('%s: /rules/%d: blacklisted', this.name, id)
} else {
this.exposeRule(id, rules[id])
}
}
}
}
HueBridge.prototype.exposeRule = function (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)
)
}
}
HueBridge.prototype.resetTransitionTime = function () {
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)
}
// ===== WebSocket =============================================================
HueBridge.prototype.listen = function () {
const wsURL = 'ws://' + this.hostname + ':' + this.obj.websocketport + '/'
this.ws = new WebSocket(wsURL)
this.ws.on('open', () => {
this.log.debug(
'%s: listening on websocket ws://%s:%d/', this.name,
this.platform.maskHost(this.hostname), this.obj.websocketport
)
})
this.ws.on('message', (data, flags) => {
try {
const obj = JSON.parse(data)
if (obj.e === 'changed' && obj.t === 'event') {
let a
switch (obj.r) {
case 'lights':
a = this.lights[obj.id]
break
case 'groups':
a = this.groups[obj.id]
break
case 'sensors':
a = this.sensors[obj.id]
break
default:
break
}
if (a) {
if (obj.state !== undefined) {
this.log.debug('%s: state changed event: %j', a.name, obj.state)
a.checkState(obj.state, true)
}
if (obj.config !== undefined) {
this.log.debug('%s: config changed event: %j', a.name, obj.config)
a.checkConfig(obj.config, true)
}
}
}
} catch (e) {
this.log.error('%s: websocket error %s', this.name, formatError(e))
}
})
this.ws.on('error', (error) => {
this.log.error(
'%s: websocket communication error %s', this.name, formatError(error)
)
})
this.ws.on('close', () => {
this.log.debug(
'%s: websocket connection closed - retrying in 30 seconds', this.name
)
setTimeout(this.listen.bind(this), 30000)
})
}
// ===== Heartbeat =============================================================
HueBridge.prototype.heartbeat = async function (beat) {
if (beat % this.state.heartrate === 0 && this.request) {
this.service.getCharacteristic(my.Characteristics.LastUpdated)
.updateValue(String(new Date()).substring(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) {
this.log.error('%s: heartbeat error: %s', this.name, formatError(error))
}
}
if (beat % 600 === 0 && this.request) {
try {
for (const id in this.sensors) {
this.sensors[id].addEntry()
}
} catch (error) {
this.log.error('%s: heartbeat error:', this.name, formatError(error))
}
}
}
HueBridge.prototype.heartbeatSensors = async function (beat) {
if (this.state.sensors === 0) {
return
}
const sensors = await this.request('get', '/sensors')
for (const id in sensors) {
const a = this.sensors[id]
if (a) {
a.heartbeat(beat, sensors[id])
}
}
}
HueBridge.prototype.heartbeatConfig = async function (beat) {
if (!this.config.linkbutton) {
return
}
const config = await this.request('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
)
if (this.config.link) {
await this.request('put', '/config', { linkbutton: false })
this.state.linkbutton = false
}
} else if (this.config.link) {
const hkLink = 0
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)
}
}
}
}
HueBridge.prototype.heartbeatLights = async function (beat) {
if (this.state.lights === 0) {
return
}
const lights = await this.request('get', '/lights')
for (const id in lights) {
const a = this.lights[id]
if (a) {
a.heartbeat(beat, lights[id])
}
}
}
HueBridge.prototype.heartbeatGroups = async function (beat) {
if (this.state.groups - this.state.group0 === 0) {
return
}
const groups = await this.request('get', '/groups')
for (const id in groups) {
const a = this.groups[id]
if (a) {
a.heartbeat(beat, groups[id])
}
}
}
HueBridge.prototype.heartbeatGroup0 = async function (beat) {
if (this.state.group0 === 0) {
return
}
const group0 = await this.request('get', '/groups/0')
const a = this.groups[0]
if (a) {
a.heartbeat(beat, group0)
}
}
HueBridge.prototype.heartbeatSchedules = async function (beat) {
if (this.state.schedules === 0) {
return
}
const schedules = await this.request('get', '/schedules')
for (const id in schedules) {
const a = this.schedules[id]
if (a) {
a.heartbeat(beat, schedules[id])
}
}
}
HueBridge.prototype.heartbeatRules = async function (beat) {
if (this.state.rules === 0) {
return
}
const rules = await this.request('get', '/rules')
for (const id in rules) {
const a = this.rules[id]
if (a) {
a.heartbeat(beat, rules[id])
}
}
}
// ===== Homekit Events ========================================================
HueBridge.prototype.setHeartrate = function (rate, callback) {
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()
}
HueBridge.prototype.setLink = function (link, callback) {
link = link ? 1 : 0
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.request('put', '/config', { linkbutton: newValue }).then(() => {
this.state.linkbutton = newValue
return callback()
}).catch((error) => {
return callback(error)
})
}
HueBridge.prototype.setTransitionTime = function (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()
}
HueBridge.prototype.setRestart = function (restart, callback) {
if (!restart) {
return callback()
}
this.log.info('%s: restart', this.name)
let method = 'put'
let path = '/config'
let body = { reboot: true }
if (this.isDeconz) {
method = 'post'
path = '/config/restartapp'
body = undefined
}
this.request(method, path, body).then((obj) => {
setTimeout(() => {
this.service.setCharacteristic(my.Characteristics.Restart, false)
}, this.platform.config.resetTimeout)
return callback()
}).catch((error) => {
return callback(error)
})
}
HueBridge.prototype.identify = function (callback) {
this.log.info('%s: identify', this.name)
this.platform.identify()
this.log.info(this.idString)
callback()
}
// ===== Bridge Communication ==================================================
// Send request to bridge / gateway.
HueBridge.prototype.request = async function (method, resource, body) {
if (this.hueClient == null) {
const options = {
bridgeid: this.serialNumber,
forceHttp: this.platform.config.forceHttp,
host: this.host,
keepAlive: true,
maxSockets: this.platform.config.parallelRequests || this.config.parallelRequests,
timeout: this.platform.config.timeout
}
if (this.username !== '') {
options.username = this.username
}
this.hueClient = new HueClient(options)
await this.hueClient.connect()
}
const requestNumber = ++this.state.request
let requestMsg
requestMsg = util.format(
'%s: %s request %d: %s %s', this.name, this.type,
requestNumber, method, resource
)
if (body != null) {
requestMsg = util.format('%s %j', requestMsg, body)
}
this.log.debug(requestMsg)
try {
const response = await this.hueClient._request(method, resource, body)
this.log.debug(
'%s: %s request %d: ok', this.name, this.type, requestNumber
)
return response
} catch (error) {
if (error.code === 'ECONNRESET' || error.statusCode === 503) {
this.log.debug(requestMsg)
this.log.debug(
'%s: %s communication error: %s - retrying in %dms',
this.name, this.type, formatError(error), this.platform.config.waitTimeResend
)
await homebridgeLib.timeout(this.platform.config.waitTimeResend)
return this.request(method, resource, body)
}
this.log.error(requestMsg)
this.log.error(
'%s: %s communication error: %s', this.name, this.type, formatError(error)
)
throw new Error()
}
}