UNPKG

homebridge-wemo

Version:

Homebridge plugin to integrate Wemo devices into HomeKit.

469 lines (409 loc) 15.3 kB
import { Builder, parseStringPromise } from 'xml2js' import { decodeXML, generateRandomString, 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.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 // Add the purifier service if it doesn't already exist this.service = this.accessory.getService(this.hapServ.AirPurifier) || this.accessory.addService(this.hapServ.AirPurifier) // Add the air quality service if it doesn't already exist this.airService = this.accessory.getService(this.hapServ.AirQualitySensor) || this.accessory.addService(this.hapServ.AirQualitySensor, 'Air Quality', 'airquality') // Add the (ionizer) switch service if it doesn't already exist this.ioService = this.accessory.getService(this.hapServ.Switch) || this.accessory.addService(this.hapServ.Switch, 'Ionizer', 'ionizer') // Add the set handler to the purifier active characteristic this.service .getCharacteristic(this.hapChar.Active) .removeOnSet() .onSet(async value => this.internalStateUpdate(value)) // Add options to the purifier target state characteristic this.service .getCharacteristic(this.hapChar.TargetAirPurifierState) .updateValue(1) .setProps({ minValue: 1, maxValue: 1, validValues: [1], }) // Add the set handler to the purifier rotation speed (for mode) characteristic this.service .getCharacteristic(this.hapChar.RotationSpeed) .setProps({ minStep: 25 }) .onSet(async (value) => { await this.internalModeUpdate(value) }) // Add the FilterChangeIndication characteristic to the air purifier if it isn't already if (!this.service.testCharacteristic(this.hapChar.FilterChangeIndication)) { this.service.addCharacteristic(this.hapChar.FilterChangeIndication) } this.cacheFilterX = this.service.getCharacteristic(this.hapChar.FilterChangeIndication).value // Add the FilterLifeLevel characteristic to the air purifier if it isn't already if (!this.service.testCharacteristic(this.hapChar.FilterLifeLevel)) { this.service.addCharacteristic(this.hapChar.FilterLifeLevel) } this.cacheFilter = this.service.getCharacteristic(this.hapChar.FilterLifeLevel).value // Add the set handler to the switch (for ionizer) characteristic this.ioService.getCharacteristic(this.hapChar.On).onSet(async (value) => { await this.internalIonizerUpdate(value) }) // Add a last mode cache value if not already set if (![1, 2, 3, 4].includes(this.accessory.context.cacheLastOnMode)) { this.accessory.context.cacheLastOnMode = 1 } // Add an ionizer on/off cache value if not already set if (![0, 1].includes(this.accessory.context.cacheIonizerOn)) { this.accessory.context.cacheIonizerOn = 0 } // Some conversion objects this.aqW2HK = { 0: 5, // poor -> poor 1: 3, // moderate -> fair 2: 1, // good -> excellent } this.aqLabels = { 5: platformLang.labelPoor, 3: platformLang.labelFair, 1: platformLang.labelExc, } this.modeLabels = { 0: platformLang.labelOff, 1: platformLang.labelLow, 2: platformLang.labelMed, 3: platformLang.labelHigh, 4: platformLang.labelAuto, } // Output the customised options to the log const opts = JSON.stringify({ }) platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) // 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 'AirQuality': this.externalAirQualityUpdate(attribute.value) break case 'ExpiredFilterTime': this.externalFilterChangeUpdate(attribute.value !== 0 ? 1 : 0) break case 'FilterLife': this.externalFilterLifeUpdate(Math.round((attribute.value / 60480) * 100)) break case 'Ionizer': this.externalIonizerUpdate(attribute.value) break case 'Mode': this.externalModeUpdate(attribute.value) break default: } } 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 }) Object.keys(result.attributeList.attribute).forEach((key) => { // Only send the required attributes to the receiveDeviceUpdate function switch (result.attributeList.attribute[key].name) { case 'AirQuality': case 'ExpiredFilterTime': case 'FilterLife': case 'Ionizer': case 'Mode': this.receiveDeviceUpdate({ name: result.attributeList.attribute[key].name, value: Number.parseInt(result.attributeList.attribute[key].value, 10), }) break default: } }) } catch (err) { const eText = parseError(err, [ platformLang.timeout, platformLang.timeoutUnreach, platformLang.noService, ]) this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) } } async sendDeviceUpdate(attributes) { // Log the sending update if debug is enabled this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`) // Generate the XML to send const builder = new Builder({ rootName: 'attribute', headless: true, renderOpts: { pretty: false }, }) const xmlAttributes = Object.keys(attributes) .map(attributeKey => builder.buildObject({ name: attributeKey, value: attributes[attributeKey], })) .join('') // Send the update await this.platform.httpClient.sendDeviceUpdate( this.accessory, 'urn:Belkin:service:deviceevent:1', 'SetAttributes', { attributeList: { '#text': xmlAttributes }, }, ) } async internalStateUpdate(value) { const prevState = this.service.getCharacteristic(this.hapChar.Active).value try { // Don't continue if the state is the same as before if (value === prevState) { return } // We also want to update the mode (by rotation speed) let newSpeed = 0 if (value !== 0) { // If turning on then we want to show the last used mode (by rotation speed) switch (this.accessory.context.cacheLastOnMode) { case 2: newSpeed = 50 break case 3: newSpeed = 75 break case 4: newSpeed = 100 break default: newSpeed = 25 } } // Update the rotation speed, use setCharacteristic so the set handler is run to send updates this.service.setCharacteristic(this.hapChar.RotationSpeed, newSpeed) // Update the characteristic if we are now ON ie purifying air this.service.updateCharacteristic( this.hapChar.CurrentAirPurifierState, newSpeed === 0 ? 0 : 2, ) // Update the ionizer characteristic if the purifier is on and the ionizer was on before this.ioService.updateCharacteristic( this.hapChar.On, value === 1 && this.accessory.context.cacheIonizerOn === 1, ) } catch (err) { const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.service.updateCharacteristic(this.hapChar.Active, prevState) }, 2000) throw new this.hapErr(-70402) } } async internalModeUpdate(value) { const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value try { // Avoid multiple updates in quick succession const updateKey = generateRandomString(5) this.updateKey = updateKey await sleep(500) if (updateKey !== this.updateKey) { return } // Don't continue if the speed is the same as before if (value === prevSpeed) { return } // Generate newValue for the needed mode depending on the new rotation speed value let newValue = 0 if (value > 10 && value <= 35) { newValue = 1 } else if (value > 35 && value <= 60) { newValue = 2 } else if (value > 60 && value <= 85) { newValue = 3 } else if (value > 85) { newValue = 4 } // Send the update await this.sendDeviceUpdate({ Mode: newValue.toString(), }) // Update the cache last used mode if not turning off if (newValue !== 0) { this.accessory.context.cacheLastOnMode = newValue } // Log the new mode if appropriate this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`) } catch (err) { const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed) }, 2000) throw new this.hapErr(-70402) } } async internalIonizerUpdate(value) { const prevState = this.ioService.getCharacteristic(this.hapChar.On).value try { // If turning on, but the purifier device is off, then turn the ionizer back off if (value && this.service.getCharacteristic(this.hapChar.Active).value === 0) { await sleep(1000) this.ioService.updateCharacteristic(this.hapChar.On, false) return } // Send the update await this.sendDeviceUpdate({ Ionizer: value ? 1 : 0, }) // Update the cache state of the ionizer this.accessory.context.cacheIonizerOn = value ? 1 : 0 // Log the update if appropriate this.accessory.log(`${platformLang.curIon} [${value ? 'on' : 'off'}}]`) } catch (err) { const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.ioService.updateCharacteristic(this.hapChar.On, prevState) }, 2000) throw new this.hapErr(-70402) } } externalModeUpdate(value) { try { // We want to find a rotation speed based on the given mode let rotSpeed = 0 switch (value) { case 1: { rotSpeed = 25 break } case 2: { rotSpeed = 50 break } case 3: { rotSpeed = 75 break } case 4: { rotSpeed = 100 break } default: return } // Update the HomeKit characteristics this.service.updateCharacteristic(this.hapChar.Active, value !== 0 ? 1 : 0) this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed) // Turn the ionizer on or off based on whether the purifier is on or off if (value === 0) { this.ioService.updateCharacteristic(this.hapChar.On, false) } else { this.ioService.updateCharacteristic( this.hapChar.On, this.accessory.context.cacheIonizerOn === 1, ) this.accessory.context.cacheLastOnMode = value } // Log the change if appropriate this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`) } catch (err) { this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) } } externalAirQualityUpdate(value) { try { const newValue = this.aqW2HK[value] // Don't continue if the value is the same as before if (this.airService.getCharacteristic(this.hapChar.AirQuality).value === newValue) { return } // Update the HomeKit characteristics this.airService.updateCharacteristic(this.hapChar.AirQuality, newValue) // Log the change if appropriate this.accessory.log(`${platformLang.curAir} [${this.aqLabels[newValue]}]`) } catch (err) { this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) } } externalIonizerUpdate(value) { try { // Don't continue if the value is the same as before const state = this.ioService.getCharacteristic(this.hapChar.On).value ? 1 : 0 if (state === value) { return } // Update the HomeKit characteristics this.ioService.updateCharacteristic(this.hapChar.On, value === 1) // Update the cache value and log the change if appropriate this.accessory.context.cacheIonizerOn = value this.accessory.log(`${platformLang.curIon} [${value === 1 ? 'on' : 'off'}]`) } catch (err) { this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) } } externalFilterChangeUpdate(value) { try { // Don't continue if the value is the same as before if (value === this.cacheFilterX) { return } // Update the HomeKit characteristics this.service.updateCharacteristic(this.hapChar.FilterChangeIndication, value) // Update the cache value and log the change if appropriate this.cacheFilterX = value } catch (err) { this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) } } externalFilterLifeUpdate(value) { try { // Don't continue if the value is the same as before if (value === this.cacheFilter) { return } // Update the HomeKit characteristics this.service.updateCharacteristic(this.hapChar.FilterLifeLevel, value) // Update the cache value and log the change if appropriate this.cacheFilter = value this.accessory.log(`${platformLang.curFilter} [${value}%]`) } catch (err) { this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) } } }