From 82c6063693f57032bbbf8fbf53fc4741fff6307d Mon Sep 17 00:00:00 2001 From: David Hislop Date: Tue, 9 Feb 2021 21:22:30 +1100 Subject: [PATCH] =?UTF-8?q?Corrected=20some=20logic=20errors=20and=20added?= =?UTF-8?q?=20support=20for=20the=20Kogan=20opener.=20The=20changes=20made?= =?UTF-8?q?=20were=20as=20follows.=20Added=20test=20for=20Kogan=20manufact?= =?UTF-8?q?urer=20and=20changed=20log=20messages=20prefix=20to=20TuyaAcces?= =?UTF-8?q?sory=20with=20manufacturer=20name.=20Set=20the=20dpAction=20and?= =?UTF-8?q?=20dpStatus=20values=20based=20on=20the=20manufacturer.=20Chang?= =?UTF-8?q?ed=20=5FgetCurrentDoorState=20to=20use=20dps=20or=20changes=20a?= =?UTF-8?q?s=20the=20argument=20and=20test=20for=20a=20Kogan=20opener=20th?= =?UTF-8?q?en=20return=20manufacturer-specific=20values.=20Added=20a=20deb?= =?UTF-8?q?ug=20flag=20(name/value=20pair=20in=20the=20configuration)=20an?= =?UTF-8?q?d=20debug=20log=20function=20=5FdebugLog().=20Replaced=20all=20?= =?UTF-8?q?console.log()=20calls=20with=20=5FdebugLog()=20calls.=20The=20d?= =?UTF-8?q?ebug=20flag=20values=20are=20true=20and=20false=20and=20default?= =?UTF-8?q?=20to=20false=20if=20not=20present=20in=20the=20configuration.?= =?UTF-8?q?=20Added=20the=20Kogan=20fopen=20and=20fclose=20commands=20to?= =?UTF-8?q?=20=5FgetTargetDoorState.=20Changed=20calls=20to=20=5FgetCurren?= =?UTF-8?q?tDoorState()=20to=20use=20the=20dpStatus=20value=20either=20dir?= =?UTF-8?q?ectly=20or=20via=20dps[this.dpStatus]=20or=20changes[this.dpSta?= =?UTF-8?q?tus]=20instead=20of=20dps=20and=20state.=20In=20=5FgetTargetDoo?= =?UTF-8?q?rState(),=20changed=20from=20Action=20values=20to=20Status=20va?= =?UTF-8?q?lues.=20Added=20a=20status=20variant=20with=20the=20correct=20s?= =?UTF-8?q?pelling=20for=20'opening'=20in=20case=20Kogan=20to=20decide=20t?= =?UTF-8?q?o=20correct=20the=20incorrect=20spelling=20'openning=E2=80=99.?= =?UTF-8?q?=20Corrected=20generic=20garage=20door=20to=20use=20true=20and?= =?UTF-8?q?=20false=20for=20open/close=20and=20opened/closed.=20Removed=20?= =?UTF-8?q?the=20code=20that=20infers=20for=20generic=20doors=20the=20open?= =?UTF-8?q?ing=20and=20closing=20states=20from=20dpStatus=20and=20dpAction?= =?UTF-8?q?,=20as=20the=20dpAction=20value=20is=20now=20unavailable=20in?= =?UTF-8?q?=20=5FgetCurrentDoorState().?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/GarageDoorAccessory.js | 238 ++++++++++++++++++++++++++++++++++--- 1 file changed, 221 insertions(+), 17 deletions(-) diff --git a/lib/GarageDoorAccessory.js b/lib/GarageDoorAccessory.js index 441d603..5804b02 100644 --- a/lib/GarageDoorAccessory.js +++ b/lib/GarageDoorAccessory.js @@ -1,5 +1,27 @@ const BaseAccessory = require('./BaseAccessory'); +// define constants for Kogan garage door. +// Action +const GARAGE_DOOR_OPEN = 'open'; +const GARAGE_DOOR_CLOSE = 'close'; +const GARAGE_DOOR_FOPEN = 'fopen'; +const GARAGE_DOOR_FCLOSE = 'fclose'; + +// Status or state +// yes, 'openning' is not a mistake, that's the text from the Kogan opener +const GARAGE_DOOR_OPENED = 'opened'; +const GARAGE_DOOR_CLOSED = 'closed'; +const GARAGE_DOOR_OPENNING = 'openning'; +const GARAGE_DOOR_OPENING = + 'opening'; // 'opening' is not currently a valid value; added in case Kogan + // one day decides to correct the spelling +const GARAGE_DOOR_CLOSING = 'closing'; +// Kogan garage door appears to have no stopped status + +// Kogan manufacturer name +const GARAGE_DOOR_MANUFACTURER_KOGAN = 'Kogan'; + +// main code class GarageDoorAccessory extends BaseAccessory { static getCategory(Categories) { return Categories.GARAGE_DOOR_OPENER; @@ -17,18 +39,78 @@ class GarageDoorAccessory extends BaseAccessory { super._registerPlatformAccessory(); } + // function to return a ID string for log messages + _logPrefix() { + return '[TuyaAccessory] ' + + (this.manufacturer ? this.manufacturer + ' ' : '') + 'GarageDoor'; + } + + // function to prefix a string ID and always log to console + _alwaysLog(...args) { console.log(this._logPrefix(), ...args); } + + // function to log to console if debug is on + _debugLog(...args) { + if (this.debug) { + this._alwaysLog(...args); + } + } + + // function to return true if the garage door manufacturer is Kogan and false + // otherwise + _isKogan() { + if (this.manufacturer === GARAGE_DOOR_MANUFACTURER_KOGAN.trim()) { + return true; + } else { + return false; + } + } + _registerCharacteristics(dps) { const {Service, Characteristic} = this.hap; const service = this.accessory.getService(Service.GarageDoorOpener); this._checkServiceName(service, this.device.context.name); - this.dpAction = this._getCustomDP(this.device.context.dpAction) || '1'; - this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '2'; + // set the debug flag + if (this.device.context.debug) { + this.debug = true; + } else { + this.debug = false; + } + + // test if this is a Kogan opener, i.e. has Kogan in upper or lower case in + // the manufacturer field and set the manufacturer property for later + // comparisons. If the manufacturer field is not Kogan and is not empty, set + // the manufacturer property to that value. Otherwise, set the manufacturer + // property to a blank string. + if (this.device.context.manufacturer.trim().toLowerCase() === + GARAGE_DOOR_MANUFACTURER_KOGAN.trim().toLowerCase()) { + this.manufacturer = GARAGE_DOOR_MANUFACTURER_KOGAN.trim(); + } else if (this.device.context.manufacturer) { + this.manufacturer = this.device.context.manufacturer.trim(); + } else { + this.manufacturer = ''; + } + // set the dpAction and dpStatus values based on the manufacturer + if (this._isKogan()) { + // Kogan SmarterHome Wireless Garage Door Opener + this._debugLog( + '_registerCharacteristics setting dpAction and dpStatus for ' + + GARAGE_DOOR_MANUFACTURER_KOGAN + ' door'); + this.dpAction = this._getCustomDP(this.device.context.dpAction) || '101'; + this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '102'; + } else { + // the original garage door opener + this._debugLog( + '_registerCharacteristics setting dpAction and dpStatus for generic door'); + this.dpAction = this._getCustomDP(this.device.context.dpAction) || '1'; + this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '2'; + } this.currentOpen = Characteristic.CurrentDoorState.OPEN; this.currentOpening = Characteristic.CurrentDoorState.OPENING; this.currentClosing = Characteristic.CurrentDoorState.CLOSING; this.currentClosed = Characteristic.CurrentDoorState.CLOSED; + this.currentStopped = Characteristic.CurrentDoorState.STOPPED; this.targetOpen = Characteristic.TargetDoorState.OPEN; this.targetClosed = Characteristic.TargetDoorState.CLOSED; if (!!this.device.context.flipState) { @@ -41,52 +123,174 @@ class GarageDoorAccessory extends BaseAccessory { } const characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState) - .updateValue(this._getTargetDoorState(dps[this.dpAction])) + .updateValue(this._getTargetDoorState(dps[this.dpStatus])) .on('get', this.getTargetDoorState.bind(this)) .on('set', this.setTargetDoorState.bind(this)); const characteristicCurrentDoorState = service.getCharacteristic(Characteristic.CurrentDoorState) - .updateValue(this._getCurrentDoorState(dps)) + .updateValue(this._getCurrentDoorState(dps[this.dpStatus])) .on('get', this.getCurrentDoorState.bind(this)); - this.device.on('change', (changes, state) => { - console.log('[TuyaAccessory] GarageDoor changed: ' + JSON.stringify(state)); + this.device.on('change', changes => { + this._alwaysLog('changed:' + JSON.stringify(changes)); + + if (changes.hasOwnProperty(this.dpStatus)) { + const newCurrentDoorState = + this._getCurrentDoorState(changes[this.dpStatus]); + this._debugLog('on change new and old CurrentDoorState ' + + newCurrentDoorState + ' ' + + characteristicCurrentDoorState.value); + this._debugLog('on change old characteristicTargetDoorState ' + + characteristicTargetDoorState.value); + + if (newCurrentDoorState == this.currentOpen && + characteristicTargetDoorState.value !== this.targetOpen) + characteristicTargetDoorState.updateValue(this.targetOpen); + + if (newCurrentDoorState == this.currentClosed && + characteristicTargetDoorState.value !== this.targetClosed) + characteristicTargetDoorState.updateValue(this.targetClosed); - if (changes.hasOwnProperty(this.dpAction)) { - const newCurrentDoorState = this._getCurrentDoorState(state); if (characteristicCurrentDoorState.value !== newCurrentDoorState) characteristicCurrentDoorState.updateValue(newCurrentDoorState); } }); } getTargetDoorState(callback) { - this.getState(this.dpAction, (err, dp) => { + this.getState(this.dpStatus, (err, dp) => { if (err) return callback(err); + this._debugLog('getTargetDoorState dp ' + JSON.stringify(dp)); + callback(null, this._getTargetDoorState(dp)); }); } _getTargetDoorState(dp) { - return dp ? this.targetOpen : this.targetClosed; + this._debugLog('_getTargetDoorState dp ' + JSON.stringify(dp)); + + if (this._isKogan()) { + // translate the Kogan strings to the enumerated status values + switch (dp) { + case GARAGE_DOOR_OPENED: + case GARAGE_DOOR_OPENNING: + case GARAGE_DOOR_OPENING: + return this.targetOpen; + + case GARAGE_DOOR_CLOSED: + case GARAGE_DOOR_CLOSING: + return this.targetClosed; + + default: + this._alwaysLog('_getTargetDoorState UNKNOWN STATE ' + + JSON.stringify(dp)); + } + } else { + // Generic garage door uses true for the opened status and false for the + // closed status + if (dp === true) { + return this.targetOpen; + } else if (dp === false) { + return this.targetClosed; + } else { + this._alwaysLog('_getTargetDoorState UNKNOWN STATE ' + + JSON.stringify(dp)); + } + } } setTargetDoorState(value, callback) { - this.setState(this.dpAction, value === this.targetOpen, callback); + var newValue = GARAGE_DOOR_CLOSE; + this._debugLog('setTargetDoorState value ' + value + ' targetOpen ' + + this.targetOpen + ' targetClosed ' + this.targetClosed); + + if (this._isKogan()) { + // translate the the enumerated status values to Kogan strings + switch (value) { + case this.targetOpen: + newValue = GARAGE_DOOR_OPEN; + break; + + case this.targetClosed: + newValue = GARAGE_DOOR_CLOSE; + break; + + default: + this._alwaysLog('setTargetDoorState UNKNOWN STATE ' + + JSON.stringify(value)); + } + } else { + // Generic garage door uses true for the open action and false for the + // close action + switch (value) { + case this.targetOpen: + newValue = true; + break; + + case this.targetClosed: + newValue = false; + break; + + default: + this._alwaysLog('setTargetDoorState UNKNOWN STATE ' + + JSON.stringify(value)); + } + } + + this.setState(this.dpAction, newValue, callback); } getCurrentDoorState(callback) { - this.getState([this.dpAction, this.dpStatus], (err, dps) => { + this.getState(this.dpStatus, (err, dpStatusValue) => { if (err) return callback(err); - callback(null, this._getCurrentDoorState(dps)); + callback(null, this._getCurrentDoorState(dpStatusValue)); }); } - _getCurrentDoorState(dps) { - // ToDo: Check other `dps` for opening and closing states - return dps[this.dpAction] ? this.currentOpen : this.currentClosed; + _getCurrentDoorState(dpStatusValue) { + this._debugLog('_getCurrentDoorState dpStatusValue ' + + JSON.stringify(dpStatusValue)); + + if (this._isKogan()) { + // translate the Kogan strings to the enumerated status values + switch (dpStatusValue) { + case GARAGE_DOOR_OPENED: + return this.currentOpen; + + case GARAGE_DOOR_OPENNING: + case GARAGE_DOOR_OPENING: + return this.currentOpening; + + case GARAGE_DOOR_CLOSING: + return this.currentClosing; + + case GARAGE_DOOR_CLOSED: + return this.currentClosed; + + default: + this._alwaysLog('_getCurrentDoorState UNKNOWN STATUS ' + + JSON.stringify(dpStatusValue)); + } + } else { + // Generic garage door uses true for the open status and false for the + // close status. It doesn't seem to have other values for opening and + // closing. If the getState() function callback in BaseAccessory.js passed + // the dps object into this function, we may be able to infer opening and + // closing from the combined dpStatus and dpAction values. That would + // require mods to every accessory that used that callback. Not worth it. + if (dpStatusValue === true) { + // dpStatus true corresponds to an open door + return this.currentOpen; + } else if (dpStatusValue === false) { + // dpStatus false corresponds to a closed door, so assume "not open" + return this.currentClosed; + } else { + this._alwaysLog('_getCurrentDoorState UNKNOWN STATUS ' + + JSON.stringify(dps[this.dpStatus])); + } + } } } -module.exports = GarageDoorAccessory; \ No newline at end of file +module.exports = GarageDoorAccessory;