From a9a041ed9402ff6e73b8453cffba2c6e36e1b8e9 Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Sat, 15 Aug 2020 17:52:32 -0400 Subject: [PATCH 01/12] Bug fixes and new feature Fix for #17: confirmed fixed for fahrenheit units, TBD for celsius Fix for #9: duplicate of #17 Fix for #16: requires user to choose in Homebridge UI since HomeKit cannot support custom characteristics (e.g. a switch for this) Fix for #18 Fix for #19 Plus, new feature: display filter cleaning status --- config.schema.json | 5 + src/characteristic/abstractCharacteristic.ts | 125 ++++++++++++++++-- ...SplitTemperatureThresholdCharacteristic.ts | 63 +++++---- src/characteristic/activeCharacteristic.ts | 6 +- ...olingThresholdTemperatureCharacteristic.ts | 5 +- .../currentTemperatureCharacteristic.ts | 14 +- .../filterChangeCharacteristic.ts | 41 ++++++ ...atingThresholdTemperatureCharacteristic.ts | 5 +- .../rotationSpeedCharacteristic.ts | 7 +- src/characteristic/swingModeCharacteristic.ts | 5 +- .../targetHeaterCoolerStateCharacteristic.ts | 42 +++++- src/getCharacteristicsForModel.ts | 54 ++++---- src/platform.ts | 39 ++++-- src/platformAccessory.ts | 56 +++++--- src/thinq/api.ts | 12 ++ src/thinq/apiTypes.ts | 19 ++- src/thinq/thinqConfig.ts | 5 +- 17 files changed, 383 insertions(+), 120 deletions(-) create mode 100644 src/characteristic/filterChangeCharacteristic.ts diff --git a/config.schema.json b/config.schema.json index bd2d28b..bef31c9 100644 --- a/config.schema.json +++ b/config.schema.json @@ -49,6 +49,11 @@ "type": "string", "required": false }, + "use_eco_mode": { + "title": "Use EcoMode (when available)", + "type": "boolean", + "required": false + }, "refresh_interval": { "title": "Refresh interval (requires Homebridge restart)", "type": "number", diff --git a/src/characteristic/abstractCharacteristic.ts b/src/characteristic/abstractCharacteristic.ts index 840b7ca..ae63fbb 100644 --- a/src/characteristic/abstractCharacteristic.ts +++ b/src/characteristic/abstractCharacteristic.ts @@ -4,14 +4,18 @@ import type { // eslint-disable-next-line @typescript-eslint/no-unused-vars Characteristic as HomebridgeCharacteristic, CharacteristicValue, + CharacteristicChange, CharacteristicGetCallback, CharacteristicSetCallback, WithUUID, } from 'homebridge' import { HomebridgeLgThinqPlatform } from '../platform' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' import type { GetDeviceResponse } from '../thinq/apiTypes' +type Unpacked = T extends (infer U)[] ? U : T + export default abstract class AbstractCharacteristic< State extends CharacteristicValue, ApiValue extends string | number, @@ -19,11 +23,13 @@ export default abstract class AbstractCharacteristic< new (): HomebridgeCharacteristic }> /** Comes from this.platform.Characteristic.____ */ > { - private platform: HomebridgeLgThinqPlatform - private service: Service - private deviceId: string + protected platform: HomebridgeLgThinqPlatform + protected service: Service + protected device: LgAirConditionerPlatformAccessory + protected characteristic: Characteristic /** Comes from this.platform.Characteristic.____ */ + private cachedState?: State - characteristic: Characteristic /** Comes from this.platform.Characteristic.____ */ + private apiCommand: 'Set' | 'Operation' private apiDataKey: keyof GetDeviceResponse['result']['snapshot'] @@ -34,14 +40,14 @@ export default abstract class AbstractCharacteristic< constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, characteristic: Characteristic, apiCommand: 'Set' | 'Operation', apiDataKey: keyof GetDeviceResponse['result']['snapshot'], ) { this.platform = platform this.service = service - this.deviceId = deviceId + this.device = device this.characteristic = characteristic this.apiCommand = apiCommand this.apiDataKey = apiDataKey @@ -56,6 +62,12 @@ export default abstract class AbstractCharacteristic< .getCharacteristic(this.characteristic) .on(CharacteristicEventTypes.SET, this.handleSet.bind(this)) } + + if (this.handleChange) { + this.service + .getCharacteristic(this.characteristic) + .on(CharacteristicEventTypes.CHANGE, this.handleChange.bind(this)) + } } /** Transform Homebridge state to what the ThinQ API expects */ @@ -66,11 +78,18 @@ export default abstract class AbstractCharacteristic< */ abstract getApiValueFromState(state: State): ApiValue + getUUID(): string { + return this.characteristic.UUID + } + /** Take in an updated device snapshot */ - handleUpdatedSnapshot(snapshot: GetDeviceResponse['result']['snapshot']) { + handleUpdatedSnapshot( + snapshot: Unpacked, + ) { try { + this.logDebug('HandleSnapshot for ' + this.characteristic.name) + const apiValue = snapshot[this.apiDataKey] as ApiValue - this.logDebug('handleUpdatedSnapshot', apiValue) this.cachedState = this.getStateFromApiValue(apiValue) this.service.updateCharacteristic(this.characteristic, this.cachedState) } catch (error) { @@ -78,9 +97,13 @@ export default abstract class AbstractCharacteristic< } } + /** Handle a "change" command from Homebridge to update this characteristic */ + handleChange?(value: CharacteristicChange) { + this.logDebug('Triggered CHANGE:', value.newValue) + } + /** Handle a "set" command from Homebridge to update this characteristic */ handleSet?(value: CharacteristicValue, callback: CharacteristicSetCallback) { - this.logDebug('Triggered SET:', value) if (!this.thinqApi) { this.logError('API not initialized yet') return @@ -92,9 +115,9 @@ export default abstract class AbstractCharacteristic< ) this.logDebug('targetState', targetState) + // The air conditioner will make a sound every time this API is called. + // To avoid unnecessary chimes, we'll optimistically skip sending the API call. if (targetState === this.cachedState) { - // The air conditioner will make a sound every time this API is called. - // To avoid unnecessary chimes, we'll optimistically skip sending the API call. this.logDebug('State equals cached state. Skipping.', targetState) callback(null, targetState) return @@ -103,14 +126,22 @@ export default abstract class AbstractCharacteristic< const apiValue = this.getApiValueFromState(targetState) this.thinqApi - .sendCommand(this.deviceId, this.apiCommand, this.apiDataKey, apiValue) + .sendCommand( + this.device.getDeviceId() || '', + this.apiCommand, + this.apiDataKey, + apiValue, + ) .then(() => { this.cachedState = targetState callback(null, targetState) }) .catch((error) => { - this.logError('Failed to set state', targetState, error.toString()) + this.logError('Failed to set state', error.toString()) callback(error) + + // put UI back to where it was before + this.device.updateCharacteristics(true) }) } @@ -134,4 +165,72 @@ export default abstract class AbstractCharacteristic< ...parameters, ) } + + deviceUsesFahrenheit(): boolean { + return this.device.getDevice().countryCode.startsWith('US') + } + + roundHalf(r: number): number { + return Math.round(r * 2) / 2 + } + + /* + LG Air Conditioners use a lookup table to go between Celsius and Farenheit, which we request from the + LG servers as part of the GetDashboard call. There are two tables, CelToFah and FahToCel, which are *not* + inverses of each other (unfortunately). + + These tables are the same as the units use, so if you want your results to match what they physically display, + you need to use the lookup tables. + + HomeKit's API uses Celsius as its standard unit, regardless of whether the user sees Farenheit on the UI or not--that is, + conversion happens on the iPhone. + + Thus, to ensure the temperatures on the app and the unit match, we have to convert to Farenheit, + then back to Celsius for HomeKit, the latter conversion using the classic (5/9) - 32 math. + + We do all this only if the user sees Farenheit, as I *think* this issue is moot if the units show celsius? A non-US user to confirm. + */ + getHomeKitCelsiusForLGAPICelsius(_celsius: number): number { + if (!this.deviceUsesFahrenheit()) { + return _celsius + } + + const LGCelsius = this.roundHalf(_celsius) + const LGCelsiusToF = this.device.getModelInfo().Value.TempCelToFah + .value_mapping + const HKCelsius = this.roundHalf((LGCelsiusToF[LGCelsius] - 32) * (5 / 9)) + + this.logDebug( + 'getHomeKitCelsiusForLGAPICelsius in=' + _celsius + ' out=' + HKCelsius, + ) + return HKCelsius + } + + // inverse of the above + getLGAPICelsiusForHomeKitCelsius(_celsius: number): number { + if (!this.deviceUsesFahrenheit()) { + return _celsius + } + + const HKCelsiusInFarenheit: number = Math.round(_celsius * (9 / 5) + 32) + const LGCelsiusToF = this.device.getModelInfo().Value.TempCelToFah + .value_mapping + + for (const LGCelsius in LGCelsiusToF) { + const LGFarenheit: number = LGCelsiusToF[LGCelsius] + + if (LGFarenheit === HKCelsiusInFarenheit) { + this.logDebug( + 'getLGAPICelsiusForHomeKitCelsius in=' + + _celsius + + ' out=' + + LGCelsius, + ) + + return Number(LGCelsius) + } + } + + return -1 + } } diff --git a/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts b/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts index 2447ada..76fdea5 100644 --- a/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts +++ b/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts @@ -1,6 +1,11 @@ -import type { Service, Characteristic } from 'homebridge' +import type { + Service, + Characteristic, + CharacteristicValue, + CharacteristicSetCallback, +} from 'homebridge' -import type { GetDeviceResponse } from '../thinq/apiTypes' +import { LgAirConditionerPlatformAccessory } from '../platformAccessory' import { HomebridgeLgThinqPlatform } from '../platform' import AbstractCharacteristic from './abstractCharacteristic' @@ -22,58 +27,62 @@ export default class AbstractSplithresholdCharacteristic extends AbstractCharact typeof Characteristic.CoolingThresholdTemperature > { mode: Mode - localPlatform: HomebridgeLgThinqPlatform - localService: Service constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, mode: Mode, ) { super( platform, service, - deviceId, + device, mode === 'cool' ? platform.Characteristic.CoolingThresholdTemperature : platform.Characteristic.HeatingThresholdTemperature, 'Set', 'airState.tempState.target', ) + this.mode = mode service .getCharacteristic(this.characteristic) // min/max as defined in product manual - .setProps({ minValue: 16, maxValue: 30, minStep: 0.5 }) - // Usually these would be private, but this is a special characteristic - // that needs these - this.localPlatform = platform - this.localService = service + .setProps({ + minValue: 16, + maxValue: 30, + minStep: 0.5, + }) } - // Override default handleUpdatedSnapshot() to ignore based on mode - handleUpdatedSnapshot(snapshot: GetDeviceResponse['result']['snapshot']) { - const targetState = this.localService.getCharacteristic( - this.localPlatform.Characteristic.TargetHeaterCoolerState, - ).value - const requiredState = - this.mode === 'cool' - ? this.localPlatform.Characteristic.TargetHeaterCoolerState.COOL - : this.localPlatform.Characteristic.TargetHeaterCoolerState.HEAT - if (targetState !== requiredState) { - this.logDebug( - `Target state is not "${this.mode}", ignoring snapshot update`, - ) + handleSet?(value: CharacteristicValue, callback: CharacteristicSetCallback) { + if (this.device.lockTemperature) { + // updating from cache when we're locked puts things back to where they were, essentially preventing edits + this.device.updateCharacteristics(true) + } else { + if (super.handleSet) { + super.handleSet(value, callback) + } } - super.handleUpdatedSnapshot(snapshot) } getStateFromApiValue(apiValue: ApiValue): State { - return apiValue + // if we're "locked", i.e. in auto mode, show the full range of temperature to + // show the user that we're not heating/cooling to any set point + if (this.device.lockTemperature) { + this.logDebug('Returning locked temperature values') + if (this.mode === 'cool') { + return 30 + } else { + return 16 + } + } else { + return this.getHomeKitCelsiusForLGAPICelsius(apiValue) + } } getApiValueFromState(state: State): ApiValue { - return state + return this.getLGAPICelsiusForHomeKitCelsius(state) } } diff --git a/src/characteristic/activeCharacteristic.ts b/src/characteristic/activeCharacteristic.ts index eca0baa..25d78c6 100644 --- a/src/characteristic/activeCharacteristic.ts +++ b/src/characteristic/activeCharacteristic.ts @@ -1,6 +1,8 @@ import type { Service, Characteristic } from 'homebridge' import { HomebridgeLgThinqPlatform } from '../platform' +import { LgAirConditionerPlatformAccessory } from '../platformAccessory' + import AbstractCharacteristic from './abstractCharacteristic' type State = @@ -17,12 +19,12 @@ export default class ActiveCharacteristic extends AbstractCharacteristic< constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, ) { super( platform, service, - deviceId, + device, platform.Characteristic.Active, 'Operation', 'airState.operation', diff --git a/src/characteristic/coolingThresholdTemperatureCharacteristic.ts b/src/characteristic/coolingThresholdTemperatureCharacteristic.ts index 4a29b17..f2e1ddb 100644 --- a/src/characteristic/coolingThresholdTemperatureCharacteristic.ts +++ b/src/characteristic/coolingThresholdTemperatureCharacteristic.ts @@ -1,4 +1,5 @@ import type { Service } from 'homebridge' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' import { HomebridgeLgThinqPlatform } from '../platform' import AbstractSplithresholdCharacteristic from './abstractSplitTemperatureThresholdCharacteristic' @@ -7,8 +8,8 @@ export default class CoolingThresholdCharacteristic extends AbstractSplithreshol constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, ) { - super(platform, service, deviceId, 'cool') + super(platform, service, device, 'cool') } } diff --git a/src/characteristic/currentTemperatureCharacteristic.ts b/src/characteristic/currentTemperatureCharacteristic.ts index 9f5102f..8b5d88a 100644 --- a/src/characteristic/currentTemperatureCharacteristic.ts +++ b/src/characteristic/currentTemperatureCharacteristic.ts @@ -1,4 +1,5 @@ import type { Service, Characteristic } from 'homebridge' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' import { HomebridgeLgThinqPlatform } from '../platform' import AbstractCharacteristic from './abstractCharacteristic' @@ -15,12 +16,12 @@ export default class CurrentTemperatureCharacteristic extends AbstractCharacteri constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, ) { super( platform, service, - deviceId, + device, platform.Characteristic.CurrentTemperature, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore This won't be used for a read-only characteristic @@ -32,11 +33,12 @@ export default class CurrentTemperatureCharacteristic extends AbstractCharacteri // Disable "set" control handleSet = undefined - getStateFromApiValue(apiValue: ApiValue): State { - return apiValue - } - + // API is never set, so this is not used getApiValueFromState(state: State): ApiValue { return state } + + getStateFromApiValue(apiValue: ApiValue): State { + return this.getHomeKitCelsiusForLGAPICelsius(apiValue) + } } diff --git a/src/characteristic/filterChangeCharacteristic.ts b/src/characteristic/filterChangeCharacteristic.ts new file mode 100644 index 0000000..a9078ce --- /dev/null +++ b/src/characteristic/filterChangeCharacteristic.ts @@ -0,0 +1,41 @@ +import type { Service, Characteristic } from 'homebridge' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' + +import { HomebridgeLgThinqPlatform } from '../platform' +import AbstractCharacteristic from './abstractCharacteristic' + +type State = number + +type ApiValue = 0 | 1 + +export default class FilterChangeCharacteristic extends AbstractCharacteristic< + State, + ApiValue, + typeof Characteristic.FilterChangeIndication +> { + constructor( + platform: HomebridgeLgThinqPlatform, + service: Service, + device: LgAirConditionerPlatformAccessory, + ) { + super( + platform, + service, + device, + platform.Characteristic.FilterChangeIndication, + 'Operation', + 'airState.filterMngStates.useTime', + ) + } + + getStateFromApiValue(apiValue: ApiValue): State { + return Number(apiValue) === 0 ? 1 : 0 + } + + handleSet = undefined + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getApiValueFromState(state: State): ApiValue { + return Number(state) === 0 ? 1 : 0 + } +} diff --git a/src/characteristic/heatingThresholdTemperatureCharacteristic.ts b/src/characteristic/heatingThresholdTemperatureCharacteristic.ts index 7d847a2..fe876b8 100644 --- a/src/characteristic/heatingThresholdTemperatureCharacteristic.ts +++ b/src/characteristic/heatingThresholdTemperatureCharacteristic.ts @@ -1,4 +1,5 @@ import type { Service } from 'homebridge' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' import { HomebridgeLgThinqPlatform } from '../platform' import AbstractSplithresholdCharacteristic from './abstractSplitTemperatureThresholdCharacteristic' @@ -7,8 +8,8 @@ export default class HeatingThresholdCharacteristic extends AbstractSplithreshol constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, ) { - super(platform, service, deviceId, 'heat') + super(platform, service, device, 'heat') } } diff --git a/src/characteristic/rotationSpeedCharacteristic.ts b/src/characteristic/rotationSpeedCharacteristic.ts index 2cdad6a..7a023cb 100644 --- a/src/characteristic/rotationSpeedCharacteristic.ts +++ b/src/characteristic/rotationSpeedCharacteristic.ts @@ -1,4 +1,5 @@ import type { Service, Characteristic } from 'homebridge' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' import { HomebridgeLgThinqPlatform } from '../platform' import AbstractCharacteristic from './abstractCharacteristic' @@ -20,13 +21,13 @@ export default class RotationSpeedCharacteristic extends AbstractCharacteristic< constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, numSpeeds = 3, ) { super( platform, service, - deviceId, + device, platform.Characteristic.RotationSpeed, 'Set', 'airState.windStrength', @@ -38,7 +39,7 @@ export default class RotationSpeedCharacteristic extends AbstractCharacteristic< .getCharacteristic(this.characteristic) // minStep 0.1 to help avoid accidentally setting state = 0. // If Homekit notices a 0 value, it also sends Active = 0 to shut it off. - .setProps({ minValue: 0, maxValue, minStep: 0.1 }) + .setProps({ minValue: 0, maxValue: maxValue, minStep: 0.1 }) } getStateFromApiValue(apiValue: ApiValue): State { diff --git a/src/characteristic/swingModeCharacteristic.ts b/src/characteristic/swingModeCharacteristic.ts index 3d3d4ca..d8b2ba5 100644 --- a/src/characteristic/swingModeCharacteristic.ts +++ b/src/characteristic/swingModeCharacteristic.ts @@ -1,4 +1,5 @@ import type { Service, Characteristic } from 'homebridge' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' import { HomebridgeLgThinqPlatform } from '../platform' import AbstractCharacteristic from './abstractCharacteristic' @@ -17,12 +18,12 @@ export default class SwingModeCharacteristic extends AbstractCharacteristic< constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, ) { super( platform, service, - deviceId, + device, platform.Characteristic.SwingMode, 'Set', 'airState.wDir.vStep', diff --git a/src/characteristic/targetHeaterCoolerStateCharacteristic.ts b/src/characteristic/targetHeaterCoolerStateCharacteristic.ts index 0aa406b..82b3013 100644 --- a/src/characteristic/targetHeaterCoolerStateCharacteristic.ts +++ b/src/characteristic/targetHeaterCoolerStateCharacteristic.ts @@ -1,6 +1,9 @@ -import type { Service, Characteristic } from 'homebridge' +import type { Service, Characteristic, CharacteristicChange } from 'homebridge' +import { GetDeviceResponse } from '../thinq/apiTypes' import { HomebridgeLgThinqPlatform } from '../platform' +import type { LgAirConditionerPlatformAccessory } from '../platformAccessory' + import AbstractCharacteristic from './abstractCharacteristic' type State = @@ -8,7 +11,7 @@ type State = | typeof Characteristic.TargetHeaterCoolerState.HEAT | typeof Characteristic.TargetHeaterCoolerState.AUTO -type ApiValue = 0 | 1 | 2 | 4 +type ApiValue = 0 | 1 | 2 | 4 | 8 export default class TargetHeaterCoolerStateCharacteristic extends AbstractCharacteristic< State, @@ -16,22 +19,24 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara typeof Characteristic.TargetHeaterCoolerState > { deviceSupportsHeat: boolean + useEcoMode: boolean constructor( platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, deviceSupportsHeat = false, ) { super( platform, service, - deviceId, + device, platform.Characteristic.TargetHeaterCoolerState, 'Set', 'airState.opMode', ) this.deviceSupportsHeat = deviceSupportsHeat + this.useEcoMode = platform.config.use_eco_mode if (this.deviceSupportsHeat) { this.logError( @@ -41,9 +46,33 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara } } + handleUpdatedSnapshot(snapshot: GetDeviceResponse['result']['snapshot']) { + super.handleUpdatedSnapshot(snapshot) + + // this sets upon initial load--event handlers will keep this set correctly after that. 2 = auto + if (snapshot['airState.opMode'] === 2) { + this.device.lockTemperature = true + } + } + + handleChange(v: CharacteristicChange) { + if (v.newValue === this.characteristic.AUTO) { + this.device.lockTemperature = true + } else { + this.device.lockTemperature = false + } + + // refresh UI each time the mode changes since the temperature can change when the mode is switched + this.device.updateCharacteristics( + v.newValue === this.characteristic.AUTO ? true : false, + this.platform.Characteristic.TargetHeaterCoolerState.UUID, + ) + } + getStateFromApiValue(apiValue: ApiValue): State { if (this.deviceSupportsHeat) { switch (apiValue) { + case 8: case 0: return this.characteristic.COOL case 4: @@ -55,6 +84,7 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara } } else { switch (apiValue) { + case 8: case 0: return this.characteristic.COOL case 1: @@ -71,7 +101,7 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara if (this.deviceSupportsHeat) { switch (state) { case this.characteristic.COOL: - return 0 + return this.useEcoMode === true ? 8 : 0 case this.characteristic.HEAT: return 4 case this.characteristic.AUTO: @@ -82,7 +112,7 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara } else { switch (state) { case this.characteristic.COOL: - return 0 + return this.useEcoMode === true ? 8 : 0 case this.characteristic.HEAT: return 1 case this.characteristic.AUTO: diff --git a/src/getCharacteristicsForModel.ts b/src/getCharacteristicsForModel.ts index 3b1a4c0..99bf2a5 100644 --- a/src/getCharacteristicsForModel.ts +++ b/src/getCharacteristicsForModel.ts @@ -1,6 +1,6 @@ import type { Logger, Service } from 'homebridge' - import type { HomebridgeLgThinqPlatform } from './platform' +import type { LgAirConditionerPlatformAccessory } from './platformAccessory' import ActiveCharacteristic from './characteristic/activeCharacteristic' import AbstractCharacteristic from './characteristic/abstractCharacteristic' @@ -10,58 +10,64 @@ import CoolingThresholdTemperatureCharacteristic from './characteristic/coolingT import HeatingThresholdTemperatureCharacteristic from './characteristic/heatingThresholdTemperatureCharacteristic' import TargetHeaterCoolerStateCharacteristic from './characteristic/targetHeaterCoolerStateCharacteristic' import CurrentTemperatureCharacteristic from './characteristic/currentTemperatureCharacteristic' +import FilterChangeCharacteristic from './characteristic/FilterChangeCharacteristic' export default function getCharacteristicsForModel( - model: string, platform: HomebridgeLgThinqPlatform, service: Service, - deviceId: string, + device: LgAirConditionerPlatformAccessory, log: Logger, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Array> { + const model = device.getDevice().modelName + switch (model) { case 'RAC_056905_WW': return [ - new ActiveCharacteristic(platform, service, deviceId), - new SwingModeCharacteristic(platform, service, deviceId), - new RotationSpeedCharacteristic(platform, service, deviceId, 4), + new ActiveCharacteristic(platform, service, device), + new SwingModeCharacteristic(platform, service, device), + new RotationSpeedCharacteristic(platform, service, device), + new FilterChangeCharacteristic(platform, service, device), + new CurrentTemperatureCharacteristic(platform, service, device), new CoolingThresholdTemperatureCharacteristic( platform, service, - deviceId, + device, ), new HeatingThresholdTemperatureCharacteristic( platform, service, - deviceId, + device, ), new TargetHeaterCoolerStateCharacteristic( platform, service, - deviceId, + device, true, ), - new CurrentTemperatureCharacteristic(platform, service, deviceId), ] + // LW8017ERSM -- 3 fan modes // LW1517IVSM -- 4 fan modes case 'WIN_056905_WW': return [ - new ActiveCharacteristic(platform, service, deviceId), - new RotationSpeedCharacteristic(platform, service, deviceId), + new ActiveCharacteristic(platform, service, device), + new RotationSpeedCharacteristic(platform, service, device), + new FilterChangeCharacteristic(platform, service, device), + new CurrentTemperatureCharacteristic(platform, service, device), new CoolingThresholdTemperatureCharacteristic( platform, service, - deviceId, + device, ), new HeatingThresholdTemperatureCharacteristic( platform, service, - deviceId, + device, ), - new TargetHeaterCoolerStateCharacteristic(platform, service, deviceId), - new CurrentTemperatureCharacteristic(platform, service, deviceId), + new TargetHeaterCoolerStateCharacteristic(platform, service, device), ] + // LP1419IVSM case 'POT_056905_WW': default: @@ -71,22 +77,24 @@ export default function getCharacteristicsForModel( 'request support for your model. Falling back to default "POT_056905_WW" model instead.', ) } + return [ - new ActiveCharacteristic(platform, service, deviceId), - new SwingModeCharacteristic(platform, service, deviceId), - new RotationSpeedCharacteristic(platform, service, deviceId), + new ActiveCharacteristic(platform, service, device), + new RotationSpeedCharacteristic(platform, service, device), + new FilterChangeCharacteristic(platform, service, device), + new CurrentTemperatureCharacteristic(platform, service, device), new CoolingThresholdTemperatureCharacteristic( platform, service, - deviceId, + device, ), new HeatingThresholdTemperatureCharacteristic( platform, service, - deviceId, + device, ), - new TargetHeaterCoolerStateCharacteristic(platform, service, deviceId), - new CurrentTemperatureCharacteristic(platform, service, deviceId), + new TargetHeaterCoolerStateCharacteristic(platform, service, device), + new SwingModeCharacteristic(platform, service, device), ] } } diff --git a/src/platform.ts b/src/platform.ts index 1a3df64..8d12526 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -68,10 +68,6 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { configureAccessory(accessory: PlatformAccessory) { this.log.debug('Restoring accessory from cache:', accessory.displayName) - // create the accessory handler - // this is imported from `platformAccessory.ts` - new LgAirConditionerPlatformAccessory(this, accessory) - // add the restored accessory to the accessories cache so we can track if it has already been registered this.accessories.push(accessory) } @@ -86,8 +82,8 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { ) this.thinqApi = new ThinqApi(thinqConfig, this.thinqAuth) await this.inititializeAuth() - this.startRefreshTokenInterval() this.discoverDevicesWhenReady() + this.startRefreshTokenInterval() } catch (error) { this.log.error('Error initializing platform', error.toString()) this.log.debug(error) @@ -100,7 +96,9 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { // may not be guaranteed countryCode: this.config.country_code || 'US', languageCode: this.config.language_code || 'en-US', + useEcoMode: this.config.use_eco_mode || false, } + const gatewayUri = await ThinqApi.getGatewayUri(partialThinqConfig) const thinqConfig: ThinqConfig = { apiBaseUri: gatewayUri.result.thinq2Uri, @@ -109,6 +107,7 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { authorizationUri: `${gatewayUri.result.empSpxUri}/login/signIn`, countryCode: partialThinqConfig.countryCode, languageCode: partialThinqConfig.languageCode, + useEcoMode: partialThinqConfig.useEcoMode, } return thinqConfig } @@ -118,7 +117,7 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { const redirectedUrl = this.config.auth_redirected_url as unknown if (this.thinqAuth?.getIsLoggedIn()) { this.log.info('Already logged into ThinQ') - await this.refreshAuth() + this.refreshAuth() } else if (typeof redirectedUrl === 'string' && redirectedUrl !== '') { this.log.info('Initiating auth with provided redirect URL') try { @@ -140,10 +139,10 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { setInterval(() => this.refreshAuth(), AUTH_REFRESH_INTERVAL) } - private async refreshAuth() { + private refreshAuth() { this.log.debug('refreshAuth()') try { - await this.thinqAuth!.initiateRefreshToken() + this.thinqAuth!.initiateRefreshToken() this.updateAndReplaceConfig() } catch (error) { this.log.error( @@ -176,8 +175,6 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { const dashboardResponse = await this.thinqApi!.getDashboard() - this.log.debug('dashboardResponse', dashboardResponse) - this.log.info( `Discover found ${dashboardResponse.result.item.length} total devices`, ) @@ -211,6 +208,9 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { // something globally unique, but constant, for example, the device serial // number or MAC address const uuid = this.api.hap.uuid.generate(device.deviceId) + const modelInfo = await this.thinqApi!.getDeviceModelInfo( + device.modelJsonUri, + ) const matchingAccessories = this.accessories.filter( (accessory) => accessory.UUID === uuid, @@ -218,10 +218,19 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { if (matchingAccessories.length > 0) { this.log.info('Existing accessory:', device.alias) + // check that the device has not already been registered by checking the // cached devices we stored in the `configureAccessory` method above for (const accessory of matchingAccessories) { + accessory.context = {} accessory.context.device = device + accessory.context.modelInfo = modelInfo + + new LgAirConditionerPlatformAccessory( + this, + accessory, + ).updateCharacteristics() + matchedAccessories.push(accessory) } } else { @@ -230,13 +239,18 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { // create a new accessory const accessory = new this.api.platformAccessory(device.alias, uuid) - // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need + // store here so these are serialized with accessory when saved to disk + accessory.context = {} accessory.context.device = device + accessory.context.modelInfo = modelInfo // create the accessory handler // this is imported from `platformAccessory.ts` - new LgAirConditionerPlatformAccessory(this, accessory) + new LgAirConditionerPlatformAccessory( + this, + accessory, + ).updateCharacteristics() // link the accessory to your platform this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ @@ -288,7 +302,6 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { const configString = readFileSync(configPath).toString() try { const config = JSON.parse(configString) - // this.log.debug('config', config) DO NOT COMMIT THIS -- it could accidentally leak into GitHub issue reports const platforms = config.platforms.filter( (platform: Record) => platform.platform === 'LgThinqAirConditioner', diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index e975b8c..88affc0 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -1,7 +1,11 @@ import type { Service, PlatformAccessory } from 'homebridge' +import type { + GetDeviceResponse, + GetDashboardResponse, + GetModelInfoResponse, +} from './thinq/apiTypes' import { HomebridgeLgThinqPlatform } from './platform' -import { GetDashboardResponse } from './thinq/apiTypes' import AbstractCharacteristic from './characteristic/abstractCharacteristic' import getCharacteristicsForModel from './getCharacteristicsForModel' @@ -25,6 +29,10 @@ export class LgAirConditionerPlatformAccessory { return this.getDevice()?.deviceId } + getModelInfo(): GetModelInfoResponse { + return this.accessory.context.modelInfo + } + constructor( private readonly platform: HomebridgeLgThinqPlatform, private readonly accessory: PlatformAccessory, @@ -43,9 +51,11 @@ export class LgAirConditionerPlatformAccessory { this.platform.Characteristic.Name, this.getDevice()?.alias || 'Not available', ) + .setCharacteristic( + this.platform.Characteristic.SerialNumber, + 'Not available', + ) - // get the LightBulb service if it exists, otherwise create a new LightBulb service - // you can create multiple services for each accessory this.service = this.accessory.getService(this.platform.Service.HeaterCooler) ?? this.accessory.addService(this.platform.Service.HeaterCooler) @@ -53,20 +63,16 @@ export class LgAirConditionerPlatformAccessory { // To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, // when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: // this.accessory.getService('NAME') ?? this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE'); - // each service must implement at-minimum the "required characteristics" for the given service type // see https://developers.homebridge.io/#/service/Lightbulb - - const deviceId = this.getDeviceId()! this.characteristics = getCharacteristicsForModel( - model, this.platform, this.service, - deviceId, + this, this.platform.log, ) - // // create handlers for required characteristics + // create handlers for required characteristics this.updateCharacteristics() const refreshInterval = this.platform.getRefreshIntervalMinutes() this.platform.log.info( @@ -83,24 +89,36 @@ export class LgAirConditionerPlatformAccessory { ) } - async updateCharacteristics() { - if (!this.platform.thinqApi?.getIsLoggedIn()) { + async updateCharacteristics( + fromCache = false, + skipUUID: string | null = null, + ) { + if (!fromCache && !this.platform.thinqApi?.getIsLoggedIn()) { this.platform.log.debug('Not logged in; skipping updateCharacteristics()') return } try { - this.platform.log.debug('Getting device status', this.getDeviceId()) - const device = await this.platform.thinqApi.getDevice(this.getDeviceId()!) - this.platform.log.debug('device response', device) - this.platform.log.debug( - 'device response.result.snapshot', - device.result.snapshot, - ) + let device = this.getDevice().snapshot + if (!fromCache && this.platform.thinqApi) { + device = await ( + await this.platform.thinqApi.getDevice(this.getDeviceId()!) + ).result.snapshot + } for (const characteristic of this.characteristics) { + this.platform.log.debug( + 'Updating characteristic ' + characteristic.getUUID(), + ) + + if (skipUUID !== null && characteristic.getUUID() === skipUUID) { + continue + } + try { - characteristic.handleUpdatedSnapshot(device.result.snapshot) + characteristic.handleUpdatedSnapshot( + >device, + ) } catch (error) { this.platform.log.error( 'Error updating characteristic ' + characteristic.constructor.name, diff --git a/src/thinq/api.ts b/src/thinq/api.ts index 0939bba..d1c6833 100644 --- a/src/thinq/api.ts +++ b/src/thinq/api.ts @@ -5,6 +5,7 @@ import { GenericResponse, GetDashboardResponse, GetGatewayUriResponse, + GetModelInfoResponse, ApiHeaders, } from './apiTypes' import ThinqAuth from './auth' @@ -43,6 +44,17 @@ export default class ThinqApi { return response.data as GetDashboardResponse } + async getDeviceModelInfo(url: string) { + const response = await axios({ + method: 'GET', + responseType: 'json', + url: url, + headers: this.generateHeaders(), + }) + + return response.data as GetModelInfoResponse + } + async getDevice(deviceId: string) { const response = await axios({ method: 'GET', diff --git a/src/thinq/apiTypes.ts b/src/thinq/apiTypes.ts index a0d6fe8..73912bd 100644 --- a/src/thinq/apiTypes.ts +++ b/src/thinq/apiTypes.ts @@ -124,7 +124,7 @@ export type GetDashboardResponse = { langPackProductTypeVer: number langPackProductTypeUri: string deviceState: string - snapshot: GetDeviceResponse['result']['snapshot'] | {} // could be a different product + snapshot: GetDeviceResponse['result']['snapshot'] | unknown // could be a different product online: true platformType: string area: number @@ -156,6 +156,23 @@ export type GetDashboardResponse = { } } +export type GetModelInfoResponse = { + Value: { + 'support.airState.opMode': { + value_mapping: Record + } + 'support.airState.windStrength': { + value_mapping: Record + } + TempCelToFah: { + value_mapping: Record + } + TempFahToCel: { + value_mapping: Record + } + } +} + export type GetGatewayUriResponse = { resultCode: string result: { diff --git a/src/thinq/thinqConfig.ts b/src/thinq/thinqConfig.ts index 1f66830..08658a4 100644 --- a/src/thinq/thinqConfig.ts +++ b/src/thinq/thinqConfig.ts @@ -16,9 +16,12 @@ export type ThinqConfig = { /** @example "en-US" */ readonly languageCode: string + + /** @example "false" */ + readonly useEcoMode: boolean } export type PartialThinqConfig = Pick< ThinqConfig, - 'countryCode' | 'languageCode' + 'countryCode' | 'languageCode' | 'useEcoMode' > From 9d27afb60ab3ac85a1923c4999efb5a4fab0441e Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Sat, 15 Aug 2020 17:54:54 -0400 Subject: [PATCH 02/12] Add missing file Plus this file that should have been part of the last commit --- src/platformAccessory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 88affc0..fa7012d 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -20,8 +20,9 @@ export class LgAirConditionerPlatformAccessory { private service: Service // eslint-disable-next-line @typescript-eslint/no-explicit-any private characteristics: Array> + public lockTemperature = false - getDevice(): Unpacked | undefined { + getDevice(): Unpacked { return this.accessory.context.device } From e8c7e59e22a95fb402648121c5fb3228430a150e Mon Sep 17 00:00:00 2001 From: jeffmaki Date: Sat, 15 Aug 2020 22:16:12 +0000 Subject: [PATCH 03/12] Fix filename problem for case sensitive filesystems --- src/getCharacteristicsForModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/getCharacteristicsForModel.ts b/src/getCharacteristicsForModel.ts index 99bf2a5..de5523e 100644 --- a/src/getCharacteristicsForModel.ts +++ b/src/getCharacteristicsForModel.ts @@ -10,7 +10,7 @@ import CoolingThresholdTemperatureCharacteristic from './characteristic/coolingT import HeatingThresholdTemperatureCharacteristic from './characteristic/heatingThresholdTemperatureCharacteristic' import TargetHeaterCoolerStateCharacteristic from './characteristic/targetHeaterCoolerStateCharacteristic' import CurrentTemperatureCharacteristic from './characteristic/currentTemperatureCharacteristic' -import FilterChangeCharacteristic from './characteristic/FilterChangeCharacteristic' +import FilterChangeCharacteristic from './characteristic/filterChangeCharacteristic' export default function getCharacteristicsForModel( platform: HomebridgeLgThinqPlatform, From 1645f8545465444aaacc658dcd84f20928194be1 Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Mon, 17 Aug 2020 10:12:17 -0400 Subject: [PATCH 04/12] Changes in response to code review --- config.schema.json | 2 +- src/characteristic/abstractCharacteristic.ts | 27 +++++++++++++------ ...SplitTemperatureThresholdCharacteristic.ts | 11 +++++--- .../filterChangeCharacteristic.ts | 1 - src/platform.ts | 7 +++-- src/platformAccessory.ts | 6 +---- src/thinq/apiTypes.ts | 8 +++--- 7 files changed, 37 insertions(+), 25 deletions(-) diff --git a/config.schema.json b/config.schema.json index bef31c9..fc2b770 100644 --- a/config.schema.json +++ b/config.schema.json @@ -50,7 +50,7 @@ "required": false }, "use_eco_mode": { - "title": "Use EcoMode (when available)", + "title": "Use Energy Saver for cooling (when available)", "type": "boolean", "required": false }, diff --git a/src/characteristic/abstractCharacteristic.ts b/src/characteristic/abstractCharacteristic.ts index ae63fbb..416b2a1 100644 --- a/src/characteristic/abstractCharacteristic.ts +++ b/src/characteristic/abstractCharacteristic.ts @@ -90,6 +90,8 @@ export default abstract class AbstractCharacteristic< this.logDebug('HandleSnapshot for ' + this.characteristic.name) const apiValue = snapshot[this.apiDataKey] as ApiValue + this.logDebug('handleUpdatedSnapshot', apiValue) + this.cachedState = this.getStateFromApiValue(apiValue) this.service.updateCharacteristic(this.characteristic, this.cachedState) } catch (error) { @@ -104,6 +106,8 @@ export default abstract class AbstractCharacteristic< /** Handle a "set" command from Homebridge to update this characteristic */ handleSet?(value: CharacteristicValue, callback: CharacteristicSetCallback) { + this.logDebug('Triggered SET:', value) + if (!this.thinqApi) { this.logError('API not initialized yet') return @@ -137,7 +141,7 @@ export default abstract class AbstractCharacteristic< callback(null, targetState) }) .catch((error) => { - this.logError('Failed to set state', error.toString()) + this.logError('Failed to set state', targetState, error.toString()) callback(error) // put UI back to where it was before @@ -195,15 +199,21 @@ export default abstract class AbstractCharacteristic< return _celsius } - const LGCelsius = this.roundHalf(_celsius) - const LGCelsiusToF = this.device.getModelInfo().Value.TempCelToFah + const LGCelsius = this.roundHalf(_celsius) + const LGCelsiusToF:Partial> = this.device.getModelInfo().Value.TempCelToFah .value_mapping - const HKCelsius = this.roundHalf((LGCelsiusToF[LGCelsius] - 32) * (5 / 9)) - + const LGFarenheit = LGCelsiusToF[LGCelsius] + + if(LGFarenheit === undefined) { + this.logError('getHomeKitCelsiusForLGAPICelsius input temperature ' + _celsius + ' was not found in LG mapping table') + return _celsius + } + + const HKCelsius = this.roundHalf((LGFarenheit - 32) * (5 / 9)) this.logDebug( 'getHomeKitCelsiusForLGAPICelsius in=' + _celsius + ' out=' + HKCelsius, ) - return HKCelsius + return HKCelsius } // inverse of the above @@ -217,7 +227,7 @@ export default abstract class AbstractCharacteristic< .value_mapping for (const LGCelsius in LGCelsiusToF) { - const LGFarenheit: number = LGCelsiusToF[LGCelsius] + const LGFarenheit = LGCelsiusToF[LGCelsius] if (LGFarenheit === HKCelsiusInFarenheit) { this.logDebug( @@ -231,6 +241,7 @@ export default abstract class AbstractCharacteristic< } } - return -1 + this.logError("Value " + _celsius + " wasn't found in the LG mapping table.") + return _celsius } } diff --git a/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts b/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts index 76fdea5..a985471 100644 --- a/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts +++ b/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts @@ -15,6 +15,9 @@ type ApiValue = number /** temperature in celcius */ type Mode = 'cool' | 'heat' +const TEMPERATURE_MAX_VALUE_C = 30 +const TEMPERATURE_MIN_VALUE_C = 16 + /** * The air conditioner will report a single API "target temperature", while Homekit * supports a target temperature for both heat & cool simultaneously. @@ -50,8 +53,8 @@ export default class AbstractSplithresholdCharacteristic extends AbstractCharact .getCharacteristic(this.characteristic) // min/max as defined in product manual .setProps({ - minValue: 16, - maxValue: 30, + minValue: TEMPERATURE_MIN_VALUE_C, + maxValue: TEMPERATURE_MAX_VALUE_C, minStep: 0.5, }) } @@ -73,9 +76,9 @@ export default class AbstractSplithresholdCharacteristic extends AbstractCharact if (this.device.lockTemperature) { this.logDebug('Returning locked temperature values') if (this.mode === 'cool') { - return 30 + return TEMPERATURE_MAX_VALUE_C } else { - return 16 + return TEMPERATURE_MIN_VALUE_C } } else { return this.getHomeKitCelsiusForLGAPICelsius(apiValue) diff --git a/src/characteristic/filterChangeCharacteristic.ts b/src/characteristic/filterChangeCharacteristic.ts index a9078ce..980017d 100644 --- a/src/characteristic/filterChangeCharacteristic.ts +++ b/src/characteristic/filterChangeCharacteristic.ts @@ -34,7 +34,6 @@ export default class FilterChangeCharacteristic extends AbstractCharacteristic< handleSet = undefined - // eslint-disable-next-line @typescript-eslint/no-unused-vars getApiValueFromState(state: State): ApiValue { return Number(state) === 0 ? 1 : 0 } diff --git a/src/platform.ts b/src/platform.ts index 8d12526..ff5e0fd 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -139,10 +139,10 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { setInterval(() => this.refreshAuth(), AUTH_REFRESH_INTERVAL) } - private refreshAuth() { +private async refreshAuth() { this.log.debug('refreshAuth()') try { - this.thinqAuth!.initiateRefreshToken() + await this.thinqAuth!.initiateRefreshToken() this.updateAndReplaceConfig() } catch (error) { this.log.error( @@ -174,6 +174,7 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { } const dashboardResponse = await this.thinqApi!.getDashboard() + this.log.debug('dashboardResponse', dashboardResponse) this.log.info( `Discover found ${dashboardResponse.result.item.length} total devices`, @@ -302,6 +303,8 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { const configString = readFileSync(configPath).toString() try { const config = JSON.parse(configString) + // this.log.debug('config', config) DO NOT COMMIT THIS -- it could accidentally leak into GitHub issue reports + const platforms = config.platforms.filter( (platform: Record) => platform.platform === 'LgThinqAirConditioner', diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index fa7012d..e9aeeb7 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -102,16 +102,12 @@ export class LgAirConditionerPlatformAccessory { try { let device = this.getDevice().snapshot if (!fromCache && this.platform.thinqApi) { - device = await ( + device = ( await this.platform.thinqApi.getDevice(this.getDeviceId()!) ).result.snapshot } for (const characteristic of this.characteristics) { - this.platform.log.debug( - 'Updating characteristic ' + characteristic.getUUID(), - ) - if (skipUUID !== null && characteristic.getUUID() === skipUUID) { continue } diff --git a/src/thinq/apiTypes.ts b/src/thinq/apiTypes.ts index 73912bd..fb2a74b 100644 --- a/src/thinq/apiTypes.ts +++ b/src/thinq/apiTypes.ts @@ -159,16 +159,16 @@ export type GetDashboardResponse = { export type GetModelInfoResponse = { Value: { 'support.airState.opMode': { - value_mapping: Record + value_mapping: Partial> } 'support.airState.windStrength': { - value_mapping: Record + value_mapping: Partial> } TempCelToFah: { - value_mapping: Record + value_mapping: Partial> } TempFahToCel: { - value_mapping: Record + value_mapping: Partial> } } } From aee80743357e23e80cd813575f46de4e6a4f7ecd Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Mon, 17 Aug 2020 10:19:10 -0400 Subject: [PATCH 05/12] ESLint changes (forgot to enable this in VC last commit) --- src/characteristic/abstractCharacteristic.ts | 28 +++++++++++++------- src/platform.ts | 2 +- src/platformAccessory.ts | 5 ++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/characteristic/abstractCharacteristic.ts b/src/characteristic/abstractCharacteristic.ts index 416b2a1..e8e29dd 100644 --- a/src/characteristic/abstractCharacteristic.ts +++ b/src/characteristic/abstractCharacteristic.ts @@ -199,21 +199,27 @@ export default abstract class AbstractCharacteristic< return _celsius } - const LGCelsius = this.roundHalf(_celsius) - const LGCelsiusToF:Partial> = this.device.getModelInfo().Value.TempCelToFah - .value_mapping - const LGFarenheit = LGCelsiusToF[LGCelsius] - - if(LGFarenheit === undefined) { - this.logError('getHomeKitCelsiusForLGAPICelsius input temperature ' + _celsius + ' was not found in LG mapping table') + const LGCelsius = this.roundHalf(_celsius) + const LGCelsiusToF: Partial> = this.device.getModelInfo().Value.TempCelToFah.value_mapping + const LGFarenheit = LGCelsiusToF[LGCelsius] + + if (LGFarenheit === undefined) { + this.logError( + 'getHomeKitCelsiusForLGAPICelsius input temperature ' + + _celsius + + ' was not found in LG mapping table', + ) return _celsius } - + const HKCelsius = this.roundHalf((LGFarenheit - 32) * (5 / 9)) this.logDebug( 'getHomeKitCelsiusForLGAPICelsius in=' + _celsius + ' out=' + HKCelsius, ) - return HKCelsius + return HKCelsius } // inverse of the above @@ -241,7 +247,9 @@ export default abstract class AbstractCharacteristic< } } - this.logError("Value " + _celsius + " wasn't found in the LG mapping table.") + this.logError( + 'Value ' + _celsius + " wasn't found in the LG mapping table.", + ) return _celsius } } diff --git a/src/platform.ts b/src/platform.ts index ff5e0fd..1929cec 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -139,7 +139,7 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { setInterval(() => this.refreshAuth(), AUTH_REFRESH_INTERVAL) } -private async refreshAuth() { + private async refreshAuth() { this.log.debug('refreshAuth()') try { await this.thinqAuth!.initiateRefreshToken() diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index e9aeeb7..196d8af 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -102,9 +102,8 @@ export class LgAirConditionerPlatformAccessory { try { let device = this.getDevice().snapshot if (!fromCache && this.platform.thinqApi) { - device = ( - await this.platform.thinqApi.getDevice(this.getDeviceId()!) - ).result.snapshot + device = (await this.platform.thinqApi.getDevice(this.getDeviceId()!)) + .result.snapshot } for (const characteristic of this.characteristics) { From d847daa80a3597c5efb11383da713b8b38c2118b Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Mon, 17 Aug 2020 17:38:05 -0400 Subject: [PATCH 06/12] Fix bug where eco mode was selected when not available on the unit, e.g. on portable units --- .../targetHeaterCoolerStateCharacteristic.ts | 3 ++- src/getCharacteristicsForModel.ts | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/characteristic/targetHeaterCoolerStateCharacteristic.ts b/src/characteristic/targetHeaterCoolerStateCharacteristic.ts index 82b3013..f2e1f68 100644 --- a/src/characteristic/targetHeaterCoolerStateCharacteristic.ts +++ b/src/characteristic/targetHeaterCoolerStateCharacteristic.ts @@ -26,6 +26,7 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara service: Service, device: LgAirConditionerPlatformAccessory, deviceSupportsHeat = false, + useEcoMode = false, ) { super( platform, @@ -36,7 +37,7 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara 'airState.opMode', ) this.deviceSupportsHeat = deviceSupportsHeat - this.useEcoMode = platform.config.use_eco_mode + this.useEcoMode = useEcoMode if (this.deviceSupportsHeat) { this.logError( diff --git a/src/getCharacteristicsForModel.ts b/src/getCharacteristicsForModel.ts index de5523e..3f076e0 100644 --- a/src/getCharacteristicsForModel.ts +++ b/src/getCharacteristicsForModel.ts @@ -65,7 +65,13 @@ export default function getCharacteristicsForModel( service, device, ), - new TargetHeaterCoolerStateCharacteristic(platform, service, device), + new TargetHeaterCoolerStateCharacteristic( + platform, + service, + device, + false, + platform.config.use_eco_mode, + ), ] // LP1419IVSM @@ -93,7 +99,13 @@ export default function getCharacteristicsForModel( service, device, ), - new TargetHeaterCoolerStateCharacteristic(platform, service, device), + new TargetHeaterCoolerStateCharacteristic( + platform, + service, + device, + false, + false, // no eco mode + ), new SwingModeCharacteristic(platform, service, device), ] } From 055a5112dbba84d191634f6c6d081446dd226b80 Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Tue, 8 Sep 2020 22:19:24 -0400 Subject: [PATCH 07/12] Apply suggestions from code review Co-authored-by: Stuart Olivera --- src/characteristic/abstractCharacteristic.ts | 4 ++-- src/platform.ts | 11 ++--------- src/platformAccessory.ts | 12 ++++++++---- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/characteristic/abstractCharacteristic.ts b/src/characteristic/abstractCharacteristic.ts index e8e29dd..be24899 100644 --- a/src/characteristic/abstractCharacteristic.ts +++ b/src/characteristic/abstractCharacteristic.ts @@ -101,7 +101,7 @@ export default abstract class AbstractCharacteristic< /** Handle a "change" command from Homebridge to update this characteristic */ handleChange?(value: CharacteristicChange) { - this.logDebug('Triggered CHANGE:', value.newValue) + this.logDebug('Triggered CHANGE ignored! Consider implementing for this characteristic. newValue:', value.newValue) } /** Handle a "set" command from Homebridge to update this characteristic */ @@ -145,7 +145,7 @@ export default abstract class AbstractCharacteristic< callback(error) // put UI back to where it was before - this.device.updateCharacteristics(true) + callback(error, this.cachedState) }) } diff --git a/src/platform.ts b/src/platform.ts index 1929cec..db8f795 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -227,10 +227,7 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { accessory.context.device = device accessory.context.modelInfo = modelInfo - new LgAirConditionerPlatformAccessory( - this, - accessory, - ).updateCharacteristics() + new LgAirConditionerPlatformAccessory(this, accessory) matchedAccessories.push(accessory) } @@ -248,10 +245,7 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { // create the accessory handler // this is imported from `platformAccessory.ts` - new LgAirConditionerPlatformAccessory( - this, - accessory, - ).updateCharacteristics() + new LgAirConditionerPlatformAccessory(this, accessory) // link the accessory to your platform this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ @@ -304,7 +298,6 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { try { const config = JSON.parse(configString) // this.log.debug('config', config) DO NOT COMMIT THIS -- it could accidentally leak into GitHub issue reports - const platforms = config.platforms.filter( (platform: Record) => platform.platform === 'LgThinqAirConditioner', diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 196d8af..3459316 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -22,7 +22,7 @@ export class LgAirConditionerPlatformAccessory { private characteristics: Array> public lockTemperature = false - getDevice(): Unpacked { + getDevice(): Unpacked | undefined { return this.accessory.context.device } @@ -100,10 +100,14 @@ export class LgAirConditionerPlatformAccessory { } try { - let device = this.getDevice().snapshot + let snapshot = this.getDevice()!.snapshot as Unpacked< + GetDeviceResponse['result']['snapshot'] + > if (!fromCache && this.platform.thinqApi) { - device = (await this.platform.thinqApi.getDevice(this.getDeviceId()!)) - .result.snapshot + const response = await this.platform.thinqApi.getDevice( + this.getDeviceId()!, + ) + snapshot = response.result.snapshot } for (const characteristic of this.characteristics) { From 036e2a064bbfc61df521cf69e000d9cdec9806c0 Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Tue, 8 Sep 2020 22:23:06 -0400 Subject: [PATCH 08/12] lint fix after code review --- src/getCharacteristicsForModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/getCharacteristicsForModel.ts b/src/getCharacteristicsForModel.ts index 3f076e0..62623ff 100644 --- a/src/getCharacteristicsForModel.ts +++ b/src/getCharacteristicsForModel.ts @@ -19,7 +19,7 @@ export default function getCharacteristicsForModel( log: Logger, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Array> { - const model = device.getDevice().modelName + const model = device.getDevice()!.modelName switch (model) { case 'RAC_056905_WW': From c4270e77e6db5b00a0c4e1a8cfd9179ae28368a3 Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Tue, 8 Sep 2020 22:27:37 -0400 Subject: [PATCH 09/12] One more fix after code review for lint --- src/characteristic/abstractCharacteristic.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/characteristic/abstractCharacteristic.ts b/src/characteristic/abstractCharacteristic.ts index be24899..b962685 100644 --- a/src/characteristic/abstractCharacteristic.ts +++ b/src/characteristic/abstractCharacteristic.ts @@ -101,7 +101,10 @@ export default abstract class AbstractCharacteristic< /** Handle a "change" command from Homebridge to update this characteristic */ handleChange?(value: CharacteristicChange) { - this.logDebug('Triggered CHANGE ignored! Consider implementing for this characteristic. newValue:', value.newValue) + this.logDebug( + 'Triggered CHANGE ignored! Consider implementing for this characteristic. newValue:', + value.newValue, + ) } /** Handle a "set" command from Homebridge to update this characteristic */ @@ -171,7 +174,11 @@ export default abstract class AbstractCharacteristic< } deviceUsesFahrenheit(): boolean { - return this.device.getDevice().countryCode.startsWith('US') + if (this.device.getDevice()) { + return this.device.getDevice()!.countryCode.startsWith('US') + } else { + return false + } } roundHalf(r: number): number { From 304e6da50e9147466dbc246998ed32e60c3be077 Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Tue, 8 Sep 2020 22:30:22 -0400 Subject: [PATCH 10/12] Update platformAccessory.ts --- src/platformAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 3459316..3a5068f 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -117,7 +117,7 @@ export class LgAirConditionerPlatformAccessory { try { characteristic.handleUpdatedSnapshot( - >device, + >snapshot, ) } catch (error) { this.platform.log.error( From 825261eb6b2f5d21be66f5bdabb53c366bf4979e Mon Sep 17 00:00:00 2001 From: jeffmaki Date: Tue, 10 Nov 2020 14:24:50 +0000 Subject: [PATCH 11/12] Changes from code review --- src/characteristic/abstractCharacteristic.ts | 2 +- ...SplitTemperatureThresholdCharacteristic.ts | 22 +++---------------- .../targetHeaterCoolerStateCharacteristic.ts | 16 +------------- src/platform.ts | 2 +- src/platformAccessory.ts | 14 +++--------- src/thinq/apiTypes.ts | 2 +- 6 files changed, 10 insertions(+), 48 deletions(-) diff --git a/src/characteristic/abstractCharacteristic.ts b/src/characteristic/abstractCharacteristic.ts index b962685..8999031 100644 --- a/src/characteristic/abstractCharacteristic.ts +++ b/src/characteristic/abstractCharacteristic.ts @@ -148,7 +148,7 @@ export default abstract class AbstractCharacteristic< callback(error) // put UI back to where it was before - callback(error, this.cachedState) + callback(error) }) } diff --git a/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts b/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts index a985471..029a95a 100644 --- a/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts +++ b/src/characteristic/abstractSplitTemperatureThresholdCharacteristic.ts @@ -60,29 +60,13 @@ export default class AbstractSplithresholdCharacteristic extends AbstractCharact } handleSet?(value: CharacteristicValue, callback: CharacteristicSetCallback) { - if (this.device.lockTemperature) { - // updating from cache when we're locked puts things back to where they were, essentially preventing edits - this.device.updateCharacteristics(true) - } else { - if (super.handleSet) { - super.handleSet(value, callback) - } + if (super.handleSet) { + super.handleSet(value, callback) } } getStateFromApiValue(apiValue: ApiValue): State { - // if we're "locked", i.e. in auto mode, show the full range of temperature to - // show the user that we're not heating/cooling to any set point - if (this.device.lockTemperature) { - this.logDebug('Returning locked temperature values') - if (this.mode === 'cool') { - return TEMPERATURE_MAX_VALUE_C - } else { - return TEMPERATURE_MIN_VALUE_C - } - } else { - return this.getHomeKitCelsiusForLGAPICelsius(apiValue) - } + return this.getHomeKitCelsiusForLGAPICelsius(apiValue) } getApiValueFromState(state: State): ApiValue { diff --git a/src/characteristic/targetHeaterCoolerStateCharacteristic.ts b/src/characteristic/targetHeaterCoolerStateCharacteristic.ts index f2e1f68..cac81e2 100644 --- a/src/characteristic/targetHeaterCoolerStateCharacteristic.ts +++ b/src/characteristic/targetHeaterCoolerStateCharacteristic.ts @@ -49,25 +49,11 @@ export default class TargetHeaterCoolerStateCharacteristic extends AbstractChara handleUpdatedSnapshot(snapshot: GetDeviceResponse['result']['snapshot']) { super.handleUpdatedSnapshot(snapshot) - - // this sets upon initial load--event handlers will keep this set correctly after that. 2 = auto - if (snapshot['airState.opMode'] === 2) { - this.device.lockTemperature = true - } } handleChange(v: CharacteristicChange) { - if (v.newValue === this.characteristic.AUTO) { - this.device.lockTemperature = true - } else { - this.device.lockTemperature = false - } - // refresh UI each time the mode changes since the temperature can change when the mode is switched - this.device.updateCharacteristics( - v.newValue === this.characteristic.AUTO ? true : false, - this.platform.Characteristic.TargetHeaterCoolerState.UUID, - ) + this.device.updateCharacteristics() } getStateFromApiValue(apiValue: ApiValue): State { diff --git a/src/platform.ts b/src/platform.ts index db8f795..3c972be 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -117,7 +117,7 @@ export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin { const redirectedUrl = this.config.auth_redirected_url as unknown if (this.thinqAuth?.getIsLoggedIn()) { this.log.info('Already logged into ThinQ') - this.refreshAuth() + await this.refreshAuth() } else if (typeof redirectedUrl === 'string' && redirectedUrl !== '') { this.log.info('Initiating auth with provided redirect URL') try { diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 3a5068f..56f7268 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -20,7 +20,6 @@ export class LgAirConditionerPlatformAccessory { private service: Service // eslint-disable-next-line @typescript-eslint/no-explicit-any private characteristics: Array> - public lockTemperature = false getDevice(): Unpacked | undefined { return this.accessory.context.device @@ -90,11 +89,8 @@ export class LgAirConditionerPlatformAccessory { ) } - async updateCharacteristics( - fromCache = false, - skipUUID: string | null = null, - ) { - if (!fromCache && !this.platform.thinqApi?.getIsLoggedIn()) { + async updateCharacteristics() { + if (!this.platform.thinqApi?.getIsLoggedIn()) { this.platform.log.debug('Not logged in; skipping updateCharacteristics()') return } @@ -103,7 +99,7 @@ export class LgAirConditionerPlatformAccessory { let snapshot = this.getDevice()!.snapshot as Unpacked< GetDeviceResponse['result']['snapshot'] > - if (!fromCache && this.platform.thinqApi) { + if (this.platform.thinqApi) { const response = await this.platform.thinqApi.getDevice( this.getDeviceId()!, ) @@ -111,10 +107,6 @@ export class LgAirConditionerPlatformAccessory { } for (const characteristic of this.characteristics) { - if (skipUUID !== null && characteristic.getUUID() === skipUUID) { - continue - } - try { characteristic.handleUpdatedSnapshot( >snapshot, diff --git a/src/thinq/apiTypes.ts b/src/thinq/apiTypes.ts index fb2a74b..dde5d06 100644 --- a/src/thinq/apiTypes.ts +++ b/src/thinq/apiTypes.ts @@ -124,7 +124,7 @@ export type GetDashboardResponse = { langPackProductTypeVer: number langPackProductTypeUri: string deviceState: string - snapshot: GetDeviceResponse['result']['snapshot'] | unknown // could be a different product + snapshot: GetDeviceResponse['result']['snapshot'] | {} // could be a different product online: true platformType: string area: number From 40450acabc467c6225f71deba39d2d5f1cbe0548 Mon Sep 17 00:00:00 2001 From: jeffmaki Date: Tue, 10 Nov 2020 14:30:36 +0000 Subject: [PATCH 12/12] Remove duplicate call --- src/characteristic/abstractCharacteristic.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/characteristic/abstractCharacteristic.ts b/src/characteristic/abstractCharacteristic.ts index 8999031..c724c46 100644 --- a/src/characteristic/abstractCharacteristic.ts +++ b/src/characteristic/abstractCharacteristic.ts @@ -78,10 +78,6 @@ export default abstract class AbstractCharacteristic< */ abstract getApiValueFromState(state: State): ApiValue - getUUID(): string { - return this.characteristic.UUID - } - /** Take in an updated device snapshot */ handleUpdatedSnapshot( snapshot: Unpacked, @@ -146,9 +142,6 @@ export default abstract class AbstractCharacteristic< .catch((error) => { this.logError('Failed to set state', targetState, error.toString()) callback(error) - - // put UI back to where it was before - callback(error) }) }