diff --git a/README.md b/README.md index 8943fd9..ffd9d21 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ Next open the config.json that contains your Homebridge configuration and add a "minCoolingTemp": 18, "maxHeatingTemp": 30, "minHeatingTemp": 5, - "updateInterval": 60000 + "updateInterval": 60000, + "debug": false, + "dummy": false } ``` @@ -32,6 +34,8 @@ The `minCoolingTemp` field is the minimum settable temperature when in COOLING m The `maxHeatingTemp` field is the maximum settable temperature when in HEATING mode. The `minHeatingTemp` field is the minimum settable temperature when in HEATING mode. The `updateInterval` field is the interval that is used to fetch new state data from the AC unit. In milliseconds! +The `debug` field is the boolean that enables or disables debug logging, set this to false unless collecting logs. +The `dummy` field is the boolean that enables mocking out the LG API and will instead use a dummy AC unit with no network calls, only for development & testing! The initial state will be fetched shortly after booting your Homebridge instance. After that an update of the state is performed every minute. @@ -47,14 +51,12 @@ After that an update of the state is performed every minute. - Open a terminal on the device where you installed this plugin and type: `npm root -g` - Navigate to the path that the previous command has printed out - Enter the folder of the plugin to where the wideq files are: `cd homebridge-lg-airco/resources/wideq` -- Execute the command `sudo python3 example.py -c country-code -l language-code` where you should replace `country-code` and `language-code` with the respective values. - For example: `sudo python3 example.py -c BE -l en-UK` +- Execute the command `python3 example.py -c country-code -l language-code -p path-to-homebridge-folder` where you should replace `country-code`, `language-code` and `path-to-homebridge-folder` with the respective values. + For example: `python3 example.py -c BE -l en-UK -p /home/pi/.homebridge` - Follow the instructions on the screen, and paste the resulting URL back into the terminal. The command will now print out a list of all known devices for your account. If wanted select the one you want and paste the value in the `config.json` file at the `deviceId` field of the corresponding accessory definition. - It will also generate a file in which the session is stored. -- Set the file permissions on the state file with this command (osx/linux): `sudo chmod a+rw wideq_state.json` + It will also generate a file in which the session is stored in the Homebridge folder. - The plugin is now fully ready to be used in Homebridge! -- Note! If you update the plugin, make sure to re-execute the command above with your specific parameters! (because updating the plugin removes the file in which the session is stored!) This code makes use of the `WideQ` library, more information [here](https://github.com/sampsyo/wideq). Some changes have been made to the included version of the WideQ library. \ No newline at end of file diff --git a/package.json b/package.json index 32bce0a..3733945 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-lg-airco", - "version": "0.1.0-beta2", + "version": "0.1.0-beta3", "description": "Homebridge plugin to control a Smart Thinq enabled LG airco unit. Makes use of WideQ => https://github.com/sampsyo/wideq", "main": "src/index.js", "scripts": { diff --git a/resources/wideq/example.py b/resources/wideq/example.py index aa92c22..c8e7660 100755 --- a/resources/wideq/example.py +++ b/resources/wideq/example.py @@ -10,7 +10,6 @@ import logging from typing import List -STATE_FILE = 'wideq_state.json' LOGGER = logging.getLogger("wideq.example") @@ -207,11 +206,12 @@ def example_command(client, cmd, args): func(client, *args) -def example(country: str, language: str, verbose: bool, +def example(country: str, language: str, path: str, verbose: bool, cmd: str, args: List[str]) -> None: if verbose: wideq.set_log_level(logging.DEBUG) + STATE_FILE = path # Load the current state for the example. try: with open(STATE_FILE) as f: @@ -279,6 +279,11 @@ def main() -> None: help='verbose mode to help debugging', action='store_true', default=False ) + parser.add_argument( + '--path', '-p', + help='path parameter specifying where to store the wideq state', + default='wideq_state.json' + ) args = parser.parse_args() country_regex = re.compile(r"^[A-Z]{2,3}$") @@ -294,7 +299,7 @@ def main() -> None: " got: '%s'", args.language) exit(1) - example(args.country, args.language, args.verbose, args.cmd, args.args) + example(args.country, args.language, args.path, args.verbose, args.cmd, args.args) if __name__ == '__main__': diff --git a/src/lg-airco-accessory.ts b/src/lg-airco-accessory.ts index 021f78c..6c45208 100644 --- a/src/lg-airco-accessory.ts +++ b/src/lg-airco-accessory.ts @@ -19,12 +19,15 @@ import {LgAircoController} from "./lg/lg-airco-controller"; import {AsyncUtils} from "./utils/async-utils"; import {DummyController} from "./lg/dummy-controller"; import {Controller} from "./lg/controller"; +import {PythonUtils} from "./utils/python-utils"; export class LgAirCoolerAccessory implements AccessoryPlugin { private readonly hap: HAP; private readonly log: Logging; private readonly config: AccessoryConfig; + private readonly storagePath: string; + private readonly logDebug: Function; private readonly informationService: Service; private readonly heaterCoolerService: Service; @@ -39,6 +42,9 @@ export class LgAirCoolerAccessory implements AccessoryPlugin { this.hap = api.hap; this.log = log; this.config = config; + this.storagePath = api.user.storagePath() + '/wideq_state.json'; + this.logDebug = this.config.debug ? this.log : () => {}; + PythonUtils.logDebug = this.logDebug; this.informationService = new this.hap.Service.AccessoryInformation() .setCharacteristic(this.hap.Characteristic.Manufacturer, 'LG') @@ -47,8 +53,7 @@ export class LgAirCoolerAccessory implements AccessoryPlugin { this.heaterCoolerService = new this.hap.Service.HeaterCooler(this.config.name); setTimeout(async () => { - console.log(this.config); - const airCoolers: AirCooler[] = await WideqAdapter.listAirCoolers(this.config.country, this.config.language); + const airCoolers: AirCooler[] = await WideqAdapter.listAirCoolers(this.config.country, this.config.language, this.storagePath); if (airCoolers.length === 1) { this.airCooler = airCoolers[0]; @@ -68,8 +73,12 @@ export class LgAirCoolerAccessory implements AccessoryPlugin { return; } - this.controller = new LgAircoController(this.airCooler, config.updateInterval); - //this.controller = new DummyController(this.airCooler, config.updateInterval); + if (this.config.dummy) { + this.controller = new DummyController(this.airCooler, this.config.updateInterval, this.config.debug ? this.log : () => {}); + } else { + this.controller = new LgAircoController(this.airCooler, this.config.updateInterval, this.storagePath, this.config.debug, this.logDebug); + } + this.handleRotationSpeedSetWithDebounce = AsyncUtils.debounce((newFanSpeed: number) => { this.controller.setFanSpeed(WideqAdapter.percentageToFanSpeed(newFanSpeed)); }, 5000); diff --git a/src/lg/dummy-controller.ts b/src/lg/dummy-controller.ts index 9cb0b38..a62cdcd 100644 --- a/src/lg/dummy-controller.ts +++ b/src/lg/dummy-controller.ts @@ -3,7 +3,9 @@ import {Controller} from "./controller"; export class DummyController extends Controller { - constructor(airCooler: AirCooler, updateInterval: number = 30000) { + private logDebug: Function; + + constructor(airCooler: AirCooler, updateInterval: number, debugLogger: Function) { super(); this.airCooler = airCooler; @@ -19,6 +21,8 @@ export class DummyController extends Controller { this.targetCoolingTemperatureInCelsius = 18; this.targetHeatingTemperatureInCelsius = 21; + + this.logDebug = debugLogger; } public isPoweredOn(): boolean { @@ -28,12 +32,12 @@ export class DummyController extends Controller { public async setPowerState(powerOn: boolean): Promise { if (this.isOn !== powerOn) { this.isOn = powerOn; - console.log('Setting power value: ' + (powerOn ? 'ON' : 'OFF')); + this.logDebug('Setting power value: ' + (powerOn ? 'ON' : 'OFF')); } } public getMode(): Mode { - console.log('Getting mode value: ' + this.mode); + this.logDebug('Getting mode value: ' + this.mode); return this.mode; } @@ -41,7 +45,7 @@ export class DummyController extends Controller { if (this.mode !== newTargetMode) { this.isOn = true; this.mode = newTargetMode; - console.log('Setting mode value: ' + newTargetMode); + this.logDebug('Setting mode value: ' + newTargetMode); await this.setTargetTemperatureInCelsius(this.mode === Mode.COOL ? this.targetCoolingTemperatureInCelsius : this.targetHeatingTemperatureInCelsius); } else { this.setPowerState(true); @@ -49,12 +53,12 @@ export class DummyController extends Controller { } public getCurrentTemperatureInCelsius(): number { - console.log('Getting current temperature value: ' + this.currentTemperatureInCelsius); + this.logDebug('Getting current temperature value: ' + this.currentTemperatureInCelsius); return this.currentTemperatureInCelsius; } public getTargetCoolingTemperatureInCelsius(): number { - console.log('Getting target temperature value: ' + this.targetCoolingTemperatureInCelsius); + this.logDebug('Getting target temperature value: ' + this.targetCoolingTemperatureInCelsius); return this.targetCoolingTemperatureInCelsius; } @@ -70,7 +74,7 @@ export class DummyController extends Controller { } public getTargetHeatingTemperatureInCelsius(): number { - console.log('Getting target heating temperature value: ' + this.targetHeatingTemperatureInCelsius); + this.logDebug('Getting target heating temperature value: ' + this.targetHeatingTemperatureInCelsius); return this.targetHeatingTemperatureInCelsius; } @@ -89,12 +93,12 @@ export class DummyController extends Controller { if (this.targetTemperatureInCelsius !== newTargetTemperatureInCelsius) { this.isOn = true; this.targetTemperatureInCelsius = newTargetTemperatureInCelsius; - console.log('Setting temperature value: ' + newTargetTemperatureInCelsius); + this.logDebug('Setting temperature value: ' + newTargetTemperatureInCelsius); } } public getVerticalSwingMode(): VSwingMode { - console.log('Getting v-swing value: ' + this.swingModeV); + this.logDebug('Getting v-swing value: ' + this.swingModeV); return this.swingModeV; } @@ -102,12 +106,12 @@ export class DummyController extends Controller { if (this.swingModeV !== newVerticalSwingMode) { this.isOn = true; this.swingModeV = newVerticalSwingMode; - console.log('Setting swing V value: ' + newVerticalSwingMode); + this.logDebug('Setting swing V value: ' + newVerticalSwingMode); } } public getHorizontalSwingMode(): HSwingMode { - console.log('Getting h-swing value: ' + this.swingModeH); + this.logDebug('Getting h-swing value: ' + this.swingModeH); return this.swingModeH; } @@ -115,12 +119,12 @@ export class DummyController extends Controller { if (this.swingModeH !== newHorizontalSwingMode) { this.isOn = true; this.swingModeH = newHorizontalSwingMode; - console.log('Setting swing H value: ' + newHorizontalSwingMode); + this.logDebug('Setting swing H value: ' + newHorizontalSwingMode); } } public getFanSpeed(): FanSpeed { - console.log('Getting fan speed value: ' + this.fanSpeed); + this.logDebug('Getting fan speed value: ' + this.fanSpeed); return this.fanSpeed; } @@ -128,7 +132,7 @@ export class DummyController extends Controller { if (this.fanSpeed !== newFanSpeed) { this.isOn = true; this.fanSpeed = newFanSpeed; - console.log('Setting fan speed value: ' + newFanSpeed); + this.logDebug('Setting fan speed value: ' + newFanSpeed); } } } \ No newline at end of file diff --git a/src/lg/lg-airco-controller.ts b/src/lg/lg-airco-controller.ts index 006502d..f6cbfaa 100644 --- a/src/lg/lg-airco-controller.ts +++ b/src/lg/lg-airco-controller.ts @@ -5,10 +5,10 @@ export class LgAircoController extends Controller { private readonly adapter: WideqAdapter; - constructor(airCooler: AirCooler, updateInterval: number = 30000) { + constructor(airCooler: AirCooler, updateInterval: number, storagePath: string, debugEnabled: boolean, debugLogger: Function) { super(); - this.adapter = new WideqAdapter(airCooler.country, airCooler.language); + this.adapter = new WideqAdapter(airCooler.country, airCooler.language, storagePath, debugEnabled, debugLogger); this.airCooler = airCooler; this.update(); diff --git a/src/lg/wideq-adapter.ts b/src/lg/wideq-adapter.ts index 5bf23c5..0c87e92 100644 --- a/src/lg/wideq-adapter.ts +++ b/src/lg/wideq-adapter.ts @@ -9,16 +9,20 @@ export class WideqAdapter { private readonly country: string; private readonly language: string; + private readonly storagePath: string; + private readonly debug: boolean; + private readonly logDebug: Function; - //TODO: Build in some kind of queue since wideq is using and updating the one state file. Multiple concurrent python instances can give issues when they are all trying to change the state file! - - constructor(country: string, language: string) { + constructor(country: string, language: string, storagePath: string, debug: boolean, debugLogger: Function) { this.country = country; this.language = language; + this.storagePath = storagePath; + this.logDebug = debugLogger; } - public static async listAirCoolers(country: string, language: string): Promise> { - const data: string = await PythonUtils.executePython3(this.wideqFolder, this.wideqScriptFile, ['-c', country, '-l', language, '-v', 'ls']); + public static async listAirCoolers(country: string, language: string, storagePath: string): Promise> { + console.log(storagePath); + const data: string = await PythonUtils.executePython3(this.wideqFolder, this.wideqScriptFile, ['-c', country, '-l', language, '-p', storagePath, '-v', 'ls']); const devices = data.split('\n'); @@ -47,11 +51,11 @@ export class WideqAdapter { public async getStatus(deviceId: string): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'ac-mon', deviceId], true); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'ac-mon', deviceId], true); + this.logDebug(data); const dataPieces: string[] = data.split(';').map(s => s.trim()); - console.log(dataPieces); + this.logDebug(dataPieces); return { isOn: dataPieces[0].toLowerCase() === 'on', mode: (Mode)[dataPieces[1]], @@ -60,84 +64,84 @@ export class WideqAdapter { fanSpeed: (FanSpeed)[dataPieces[4].substring(10)] }; } catch (error) { - console.error(error); + this.logDebug(error); return null; } } public async getCurrentPowerUsage(deviceId: string): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'get-power-draw', deviceId]); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'get-power-draw', deviceId]); + this.logDebug(data); return parseInt(data); } catch (error) { - console.error(error); + this.logDebug(error); return null; } } public async setPowerOnOff(deviceId: string, poweredOn: boolean): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'turn', deviceId, (poweredOn ? 'on': 'off')]); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'turn', deviceId, (poweredOn ? 'on': 'off')]); + this.logDebug(data); return true; } catch (error) { - console.error(error); + this.logDebug(error); return false; } } public async setTargetTemperature(deviceId: string, temperatureInCelsius: number): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'set-temp', deviceId, (temperatureInCelsius + '')]); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'set-temp', deviceId, (temperatureInCelsius + '')]); + this.logDebug(data); return true; } catch (error) { - console.error(error); + this.logDebug(error); return false; } } public async setMode(deviceId: string, mode: Mode): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'set-mode', deviceId, mode]); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'set-mode', deviceId, mode]); + this.logDebug(data); return true; } catch (error) { - console.error(error); + this.logDebug(error); return false; } } public async setFanSpeed(deviceId: string, fanSpeed: FanSpeed): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'set-speed', deviceId, fanSpeed]); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'set-speed', deviceId, fanSpeed]); + this.logDebug(data); return true; } catch (error) { - console.error(error); + this.logDebug(error); return false; } } public async setSwingModeV(deviceId: string, swingModeV: VSwingMode): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'set-swing-v', deviceId, swingModeV]); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'set-swing-v', deviceId, swingModeV]); + this.logDebug(data); return true; } catch (error) { - console.error(error); + this.logDebug(error); return false; } } public async setSwingModeH(deviceId: string, swingModeH: HSwingMode): Promise { try { - const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-v', 'set-swing-h', deviceId, swingModeH]); - console.log(data); + const data: string = await PythonUtils.executePython3(WideqAdapter.wideqFolder, WideqAdapter.wideqScriptFile, ['-c', this.country, '-l', this.language, '-p', this.storagePath, this.debug ? '-v' : null, 'set-swing-h', deviceId, swingModeH]); + this.logDebug(data); return true; } catch (error) { - console.error(error); + this.logDebug(error); return false; } } diff --git a/src/utils/python-utils.ts b/src/utils/python-utils.ts index f8bd4e2..788d3bd 100644 --- a/src/utils/python-utils.ts +++ b/src/utils/python-utils.ts @@ -2,20 +2,24 @@ const {spawn} = require('child_process'); export class PythonUtils { + public static logDebug: Function; + public static async executePython3(workingDir: string, scriptName: string, args: string[], forceCloseProcessAfterOutput: boolean = false): Promise { const pythonArgs: string[] = []; for (const arg of args) { - pythonArgs.push(arg.trim()); + if(arg) { + pythonArgs.push(arg.trim()); + } } return new Promise((resolve, reject) => { let data: string[] = []; - console.log('python3 -u example.py ' + pythonArgs.join(' ')); + this.logDebug('python3 -u example.py ' + pythonArgs.join(' ')); const process = spawn('python3', ['-u', 'example.py'].concat(pythonArgs), {cwd: workingDir}); process.stdout.on('data', (output: any) => { - console.log(output.toString()); + this.logDebug(output.toString()); data.push(output.toString()); if (forceCloseProcessAfterOutput) { process.kill("SIGINT");