homebridge-wemo
Version:
Homebridge plugin to integrate Wemo devices into HomeKit.
427 lines (377 loc) • 16.5 kB
JavaScript
import { parseStringPromise } from 'xml2js'
import platformConsts from '../utils/constants.js'
import { decodeXML, parseError, sleep } from '../utils/functions.js'
import platformLang from '../utils/lang-en.js'
export default class {
constructor(platform, accessory) {
// Set up variables from the platform
this.eveChar = platform.eveChar
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
// Set up custom variables for this device type
const deviceConf = platform.deviceConf[accessory.context.serialNumber] || {}
this.doorOpenTimer = deviceConf.makerTimer || platformConsts.defaultValues.makerTimer
// Some conversion objects
this.gStates = {
Open: 0,
Closed: 1,
Opening: 2,
Closing: 3,
Stopped: 4,
}
// If the accessory has a switch service then remove it
if (this.accessory.getService(this.hapServ.Switch)) {
this.accessory.removeService(this.accessory.getService(this.hapServ.Switch))
}
// If the accessory has a contact sensor service then remove it
if (this.accessory.getService(this.hapServ.ContactSensor)) {
this.accessory.removeService(this.accessory.getService(this.hapServ.ContactSensor))
}
// Add the garage door service if it doesn't already exist
this.service = this.accessory.getService(this.hapServ.GarageDoorOpener)
if (!this.service) {
this.service = this.accessory.addService(this.hapServ.GarageDoorOpener)
this.service.addCharacteristic(this.eveChar.LastActivation)
this.service.addCharacteristic(this.eveChar.ResetTotal)
this.service.addCharacteristic(this.eveChar.TimesOpened)
}
// Remove unused characteristics
if (this.service.testCharacteristic(this.hapChar.ContactSensorState)) {
this.service.removeCharacteristic(
this.service.getCharacteristic(this.hapChar.ContactSensorState),
)
}
if (this.service.testCharacteristic(this.eveChar.OpenDuration)) {
this.service.removeCharacteristic(this.service.getCharacteristic(this.eveChar.OpenDuration))
}
if (this.service.testCharacteristic(this.eveChar.ClosedDuration)) {
this.service.removeCharacteristic(this.service.getCharacteristic(this.eveChar.ClosedDuration))
}
// Add the set handler to the garage door reset total characteristic
this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => {
this.service.updateCharacteristic(this.eveChar.TimesOpened, 0)
})
// Add the set handler to the target door state characteristic
this.service
.getCharacteristic(this.hapChar.TargetDoorState)
.removeOnSet()
.onSet(async value => this.internalStateUpdate(value))
// Pass the accessory to Fakegato to set up with Eve
this.accessory.eveService = new platform.eveService('door', this.accessory, {
log: () => {},
})
this.accessory.eveService.addEntry({
status: this.service.getCharacteristic(this.hapChar.CurrentDoorState).value === 0 ? 0 : 1,
})
// Output the customised options to the log
const opts = JSON.stringify({
makerTimer: this.doorOpenTimer,
})
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
// This is to remove the 'No Response' message that is there before the plugin finds this device
this.service.updateCharacteristic(
this.hapChar.TargetDoorState,
this.accessory.context.cacheLastTargetState,
)
// Request a device update immediately
this.requestDeviceUpdate()
// Start a polling interval if the user has disabled upnp
if (this.accessory.context.connection === 'http') {
this.pollingInterval = setInterval(
() => this.requestDeviceUpdate(),
platform.config.pollingInterval * 1000,
)
}
}
receiveDeviceUpdate(attribute) {
// Log the receiving update if debug is enabled
this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`)
// Check which attribute we are getting
switch (attribute.name) {
case 'Switch': {
if (attribute.value !== 0) {
this.externalStateUpdate()
}
break
}
case 'Sensor': {
this.externalSensorUpdate(attribute.value, true)
break
}
default:
}
}
async sendDeviceUpdate(value) {
// Log the sending update if debug is enabled
this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`)
// Send the update
await this.platform.httpClient.sendDeviceUpdate(
this.accessory,
'urn:Belkin:service:basicevent:1',
'SetBinaryState',
value,
)
}
async requestDeviceUpdate() {
try {
// Request the update
const data = await this.platform.httpClient.sendDeviceUpdate(
this.accessory,
'urn:Belkin:service:deviceevent:1',
'GetAttributes',
)
// Parse the response
const decoded = decodeXML(data.attributeList)
const xml = `<attributeList>${decoded}</attributeList>`
const result = await parseStringPromise(xml, { explicitArray: false })
const attributes = {}
Object.keys(result.attributeList.attribute).forEach((key) => {
const attribute = result.attributeList.attribute[key]
attributes[attribute.name] = Number.parseInt(attribute.value, 10)
})
// Only send the required attributes to the receiveDeviceUpdate function
if (attributes.SwitchMode === 0) {
this.accessory.logWarn(platformLang.makerNeedMMode)
return
}
if (attributes.SensorPresent === 1) {
this.sensorPresent = true
this.externalSensorUpdate(attributes.Sensor)
} else {
this.sensorPresent = false
}
} catch (err) {
const eText = parseError(err, [
platformLang.timeout,
platformLang.timeoutUnreach,
platformLang.noService,
])
this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`)
}
}
async internalStateUpdate(value) {
const prevTarg = this.service.getCharacteristic(this.hapChar.TargetDoorState).value
const prevCurr = this.service.getCharacteristic(this.hapChar.CurrentDoorState).value
try {
// Checks to see if the new required movement is already happening
if (this.isMoving) {
if (value === this.gStates.Closed && prevCurr === this.gStates.Closing) {
this.accessory.log(platformLang.makerClosing)
return
}
if (value === this.gStates.Open && prevCurr === this.gStates.Opening) {
this.accessory.log(platformLang.makerOpening)
return
}
} else if (value === this.gStates.Closed && prevCurr === this.gStates.Closed) {
this.accessory.log(platformLang.makerClosed)
return
} else if (value === this.gStates.Open && prevCurr === this.gStates.Open) {
this.accessory.log(platformLang.makerOpen)
return
}
// Required movement isn't already in progress so make the new movement happen
this.homekitTriggered = true
// Send the update
await this.sendDeviceUpdate({
BinaryState: 1,
})
// Log the change if appropriate
this.accessory.log(`${platformLang.tarState} [${value ? platformLang.labelClosed : platformLang.labelOpen}]`)
// Call the function to set the door moving
this.accessory.context.cacheLastTargetState = value
this.setDoorMoving(value, true)
} catch (err) {
this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
setTimeout(() => {
this.service.updateCharacteristic(this.hapChar.TargetDoorState, prevTarg)
this.accessory.context.cacheLastTargetState = prevTarg
}, 2000)
throw new this.hapErr(-70402)
}
}
externalStateUpdate() {
try {
// We want to ignore update notifications from when controlled through HomeKit
if (this.homekitTriggered) {
this.homekitTriggered = false
return
}
// The change of state must have been triggered externally
const target = this.service.getCharacteristic(this.hapChar.TargetDoorState).value
const state = 1 - target
this.accessory.log(
`${platformLang.tarState} [${state === 1 ? platformLang.labelClosed : platformLang.labelOpen}] [${platformLang.makerTrigExt}]`,
)
// Update the new target state HomeKit characteristic
this.service.updateCharacteristic(this.hapChar.TargetDoorState, state)
this.accessory.context.cacheLastTargetState = state
// If the door has been opened externally then update the Eve-only characteristics
if (state === 0) {
this.accessory.eveService.addEntry({ status: 0 })
this.service.updateCharacteristic(
this.eveChar.LastActivation,
Math.round(new Date().valueOf() / 1000) - this.accessory.eveService.getInitialTime(),
)
this.service.updateCharacteristic(
this.eveChar.TimesOpened,
this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
)
}
this.setDoorMoving(state)
} catch (err) {
this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
}
}
externalSensorUpdate(state, wasTriggered) {
try {
// 0->1 and 1->0 reverse values to match HomeKit needs
const value = 1 - state
const target = this.service.getCharacteristic(this.hapChar.TargetDoorState).value
if (target === 0) {
// CASE target is to OPEN
if (value === 0) {
// Garage door HK target state is OPEN and the sensor has reported OPEN
if (this.isMoving) {
// Garage door is in the process of opening
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.gStates.Opening)
this.accessory.eveService.addEntry({ status: 0 })
this.service.updateCharacteristic(
this.eveChar.LastActivation,
Math.round(new Date().valueOf() / 1000) - this.accessory.eveService.getInitialTime(),
)
this.service.updateCharacteristic(
this.eveChar.TimesOpened,
this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
)
// Log the change if appropriate
this.accessory.log(`${platformLang.curState} [${platformLang.labelOpening}]`)
} else {
// Garage door is open and not moving
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.gStates.Open)
// Log the change if appropriate
this.accessory.log(`${platformLang.curState} [${platformLang.labelOpen}]`)
}
} else {
// Garage door HK target state is OPEN and the sensor has reported CLOSED
// Must have been triggered externally
this.isMoving = false
this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.gStates.Closed)
this.accessory.context.cacheLastTargetState = this.gStates.Closed
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.gStates.Closed)
this.accessory.eveService.addEntry({ status: 1 })
// Log the change if appropriate
this.accessory.log(`${platformLang.curState} [${platformLang.labelClosed}] [${platformLang.makerTrigExt}]`)
}
} else if (value === 1) {
// Garage door HK target state is CLOSED and the sensor has reported CLOSED
this.isMoving = false
if (this.movingTimer) {
clearTimeout(this.movingTimer)
this.movingTimer = false
}
// Update the HomeKit characteristics
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.gStates.Closed)
this.accessory.eveService.addEntry({ status: 1 })
// Log the change if appropriate
this.accessory.log(`${platformLang.curState} [${platformLang.labelClosed}]`)
} else {
// Garage door HK target state is CLOSED but the sensor has reported OPEN
// Must have been triggered externally
this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.gStates.Open)
this.accessory.context.cacheLastTargetState = this.gStates.Open
this.accessory.eveService.addEntry({ status: 0 })
this.service.updateCharacteristic(
this.eveChar.LastActivation,
Math.round(new Date().valueOf() / 1000) - this.accessory.eveService.getInitialTime(),
)
this.service.updateCharacteristic(
this.eveChar.TimesOpened,
this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
)
// Log the change if appropriate
this.accessory.log(`${platformLang.tarState} [${platformLang.labelOpen}] [${platformLang.makerTrigExt}]`)
if (wasTriggered) {
this.setDoorMoving(0)
}
}
} catch (err) {
this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
}
}
async setDoorMoving(targetDoorState, homekitTriggered) {
// If a moving timer already exists then stop it
if (this.movingTimer) {
clearTimeout(this.movingTimer)
this.movingTimer = false
}
// The door must have stopped
if (this.isMoving) {
this.isMoving = false
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 4)
this.accessory.log(`${platformLang.curState} [${platformLang.labelStopped}]`)
// Toggle TargetDoorState after receiving a stop
await sleep(500)
const target = targetDoorState === this.gStates.Open ? this.gStates.Closed : this.gStates.Open
this.service.updateCharacteristic(this.hapChar.TargetDoorState, target)
this.accessory.context.cacheLastTargetState = target
return
}
// Set the moving flag to true
this.isMoving = true
if (homekitTriggered) {
// CASE: triggered through HomeKit
const curState = this.service.getCharacteristic(this.hapChar.CurrentDoorState).value
if (targetDoorState === this.gStates.Closed) {
// CASE: triggered through HomeKit and requested to CLOSE
if (curState !== this.gStates.Closed) {
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.gStates.Closing)
// Log the change if appropriate
this.accessory.log(`${platformLang.curState} [${platformLang.labelClosing}]`)
}
} else if (
curState === this.gStates.Stopped
|| (curState !== this.gStates.Open && !this.sensorPresent)
) {
// CASE: triggered through HomeKit and requested to OPEN
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.gStates.Opening)
this.accessory.eveService.addEntry({ status: 0 })
this.service.updateCharacteristic(
this.eveChar.LastActivation,
Math.round(new Date().valueOf() / 1000) - this.accessory.eveService.getInitialTime(),
)
this.service.updateCharacteristic(
this.eveChar.TimesOpened,
this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
)
// Log the change if appropriate
this.accessory.log(`${platformLang.curState} [${platformLang.labelOpening}]`)
}
}
// Set up the moving timer
this.movingTimer = setTimeout(() => {
this.movingTimer = false
this.isMoving = false
const target = this.service.getCharacteristic(this.hapChar.TargetDoorState).value
if (!this.sensorPresent) {
this.service.updateCharacteristic(
this.hapChar.CurrentDoorState,
target === 1 ? this.gStates.Closed : this.gStates.Open,
)
// Log the change if appropriate
this.accessory.log(`${platformLang.curState} [${target === 1 ? platformLang.labelClosed : platformLang.labelOpen}]`)
return
}
if (target === 1) {
this.accessory.eveService.addEntry({ status: 1 })
}
// Request a device update at the end of the timer
this.requestDeviceUpdate()
}, this.doorOpenTimer * 1000)
}
}