@homebridge-plugins/homebridge-govee
Version:
Homebridge plugin to integrate Govee devices into HomeKit.
301 lines (264 loc) • 10.1 kB
JavaScript
import { generateRandomString, hasProperty, parseError } from '../utils/functions.js'
import platformLang from '../utils/lang-en.js'
export default class {
constructor(platform, accessory) {
// Set up variables from the platform
this.hapChar = platform.api.hap.Characteristic
this.hapErr = platform.api.hap.HapStatusError
this.hapServ = platform.api.hap.Service
this.platform = platform
// Set up variables from the accessory
this.accessory = accessory
this.temperatureSource = accessory.context.temperatureSource;
// Remove any old services from simulations
['AirPurifier', 'Lightbulb', 'Outlet', 'Switch', 'Valve'].forEach((service) => {
if (this.accessory.getService(this.hapServ[service])) {
this.accessory.removeService(this.accessory.getService(this.hapServ[service]))
}
})
// Set up the accessory with default target temp when added the first time
if (!hasProperty(this.accessory.context, 'cacheTarget')) {
this.accessory.context.cacheTarget = 20
}
// Check to make sure user has not switched from cooler to heater
if (this.accessory.context.cacheType !== 'cooler') {
// Remove and re-setup as a HeaterCooler
if (this.accessory.getService(this.hapServ.HeaterCooler)) {
this.accessory.removeService(this.accessory.getService(this.hapServ.HeaterCooler))
}
this.accessory.context.cacheType = 'cooler'
this.accessory.context.cacheTarget = 20
}
// Add the heater service if it doesn't already exist
this.service = this.accessory.getService(this.hapServ.HeaterCooler)
|| this.accessory.addService(this.hapServ.HeaterCooler)
// Set custom properties of the current temperature characteristic
this.service.getCharacteristic(this.hapChar.CurrentTemperature).setProps({
minStep: 0.1,
})
this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
// Add the set handler to the heater active characteristic
this.service
.getCharacteristic(this.hapChar.Active)
.onSet(async value => this.internalStateUpdate(value))
// Add options to the target state characteristic
this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({
minValue: 0,
maxValue: 0,
validValues: [0],
})
// Add the set handler to the target temperature characteristic
this.service
.getCharacteristic(this.hapChar.CoolingThresholdTemperature)
.updateValue(this.accessory.context.cacheTarget)
.setProps({ minStep: 0.5 })
.onSet(async value => this.internalTargetTempUpdate(value))
// Initialise these caches now since they aren't determined by the initial externalUpdate()
this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
this.cacheCool = this.cacheState === 'on'
&& this.service.getCharacteristic(this.hapChar.CurrentHeaterCoolerState).value === 3
? 'on'
: 'off'
// Pass the accessory to Fakegato to set up with Eve
this.accessory.eveService = new platform.eveService('custom', this.accessory, {
log: () => {},
})
// Set up an interval to get regular temperature updates
setTimeout(() => {
this.getTemperature()
this.intervalPoll = setInterval(() => this.getTemperature(), 120000)
}, 5000)
// Stop the intervals on Homebridge shutdown
platform.api.on('shutdown', () => {
clearInterval(this.intervalPoll)
})
// Output the customised options to the log
const opts = JSON.stringify({
showAs: 'cooler',
temperatureSource: this.temperatureSource,
})
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
}
async internalStateUpdate(value) {
try {
let newState
let newCool
let newValue
if (value === 0) {
newValue = 'off'
newState = 'off'
newCool = 'off'
} else if (this.cacheTemp > this.accessory.context.cacheTarget) {
newValue = 'on'
newState = 'on'
newCool = 'on'
} else {
newValue = 'off'
newState = 'on'
newCool = 'off'
}
// Only send the update if either:
// * The new value (state) is OFF and the cacheCool was ON
// * The new value (state) is ON and newCool is 'on'
if ((value === 0 && this.cacheCool === 'on') || (value === 1 && newCool === 'on')) {
// Set up a one-minute timeout for the plugin to ignore incoming updates
const timerKey = generateRandomString(5)
this.updateTimeout = timerKey
setTimeout(() => {
if (this.updateTimeout === timerKey) {
this.updateTimeout = false
}
}, 60000)
// Send the request to the platform sender function
await this.platform.sendDeviceUpdate(this.accessory, {
cmd: 'stateOutlet',
value: newValue,
})
}
// Cache and log
if (newState !== this.cacheState) {
this.cacheState = newState
this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
}
if (newCool !== this.cacheCool) {
this.cacheCool = newCool
this.accessory.log(`${platformLang.curCool} [${this.cacheCool}]`)
}
const newOnState = this.cacheCool === 'on' ? 3 : 1
this.service.updateCharacteristic(
this.hapChar.CurrentHeaterCoolerState,
value === 1 ? newOnState : 0,
)
} catch (err) {
// Catch any errors during the process
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
setTimeout(() => {
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
}, 2000)
throw new this.hapErr(-70402)
}
}
async internalTargetTempUpdate(value) {
try {
// Don't continue if the new value is the same as before
if (value === this.accessory.context.cacheTarget) {
return
}
this.accessory.context.cacheTarget = value
this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
if (this.cacheState === 'off') {
return
}
// Check to see if we need to turn on or off
let newValue
let newCool
if (this.cacheTemp > value) {
newValue = 'on'
newCool = 'on'
} else {
newValue = 'off'
newCool = 'off'
}
// Don't continue if no change needed to device state
if (newCool === this.cacheCool) {
return
}
// Set up a one-minute timeout for the plugin to ignore incoming updates
const timerKey = generateRandomString(5)
this.updateTimeout = timerKey
setTimeout(() => {
if (this.updateTimeout === timerKey) {
this.updateTimeout = false
}
}, 60000)
// Send the request to the platform sender function
await this.platform.sendDeviceUpdate(this.accessory, {
cmd: 'stateOutlet',
value: newValue,
})
// Cache and log
this.cacheCool = newCool
this.accessory.log(`${platformLang.curCool} [${this.cacheCool}]`)
this.service.updateCharacteristic(
this.hapChar.CurrentHeaterCoolerState,
this.cacheCool === 'on' ? 3 : 1,
)
} catch (err) {
// Catch any errors during the process
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
setTimeout(() => {
this.service.updateCharacteristic(
this.hapChar.CoolingThresholdTemperature,
this.accessory.context.cacheTarget,
)
}, 2000)
throw new this.hapErr(-70402)
}
}
async internalCurrentTempUpdate() {
try {
// Don't continue if the device is off
if (this.cacheState === 'off') {
return
}
// Check to see if we need to turn on or off
let newValue
let newCool
if (this.cacheTemp > this.accessory.context.cacheTarget) {
newValue = 'on'
newCool = 'on'
} else {
newValue = 'off'
newCool = 'off'
}
// Don't continue if no change needed to device state
if (newCool === this.cacheCool) {
return
}
// Set up a one-minute timeout for the plugin to ignore incoming updates
const timerKey = generateRandomString(5)
this.updateTimeout = timerKey
setTimeout(() => {
if (this.updateTimeout === timerKey) {
this.updateTimeout = false
}
}, 60000)
// Send the request to the platform sender function
await this.platform.sendDeviceUpdate(this.accessory, {
cmd: 'stateOutlet',
value: newValue,
})
// Log and cache
this.cacheCool = newCool
this.accessory.log(`${platformLang.curCool} [${this.cacheCool}]`)
this.service.updateCharacteristic(
this.hapChar.CurrentHeaterCoolerState,
this.cacheCool === 'on' ? 3 : 1,
)
} catch (err) {
// Catch any errors during the process
this.accessory.logWarn(parseError(err))
}
}
async getTemperature() {
try {
// Skip polling if the storage hasn't initialised properly
if (!this.platform.storageClientData) {
return
}
const newTemp = await this.platform.storageData.getItem(`${this.temperatureSource}_temp`)
if (newTemp && newTemp !== this.cacheTemp) {
this.cacheTemp = newTemp
this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
this.accessory.eveService.addEntry({ temp: this.cacheTemp })
this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
await this.internalCurrentTempUpdate()
}
} catch (err) {
this.accessory.logWarn(parseError(err))
}
}
externalUpdate() {}
}