diff --git a/CODEOWNERS b/CODEOWNERS index 085876b540840..455883f4a48d9 100755 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -188,6 +188,7 @@ /bundles/org.openhab.binding.lcn/ @fwolter /bundles/org.openhab.binding.leapmotion/ @kaikreuzer /bundles/org.openhab.binding.lghombot/ @FluBBaOfWard +/bundles/org.openhab.binding.lgthinq/ @nemerdaud /bundles/org.openhab.binding.lgtvserial/ @fa2k /bundles/org.openhab.binding.lgwebos/ @sprehn /bundles/org.openhab.binding.lifx/ @wborn diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index e4acce9bd8d1e..e4a184c8cdc62 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -936,6 +936,11 @@ org.openhab.binding.lghombot ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.lgthinq + ${project.version} + org.openhab.addons.bundles org.openhab.binding.lgtvserial diff --git a/bundles/org.openhab.binding.lgthinq/NOTICE b/bundles/org.openhab.binding.lgthinq/NOTICE new file mode 100644 index 0000000000000..443a6b2cff1bb --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/NOTICE @@ -0,0 +1,25 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + +== Third-party Content + +jackson +* License: Apache 2.0 License +* Project: https://github.com/FasterXML/jackson +* Source: https://github.com/FasterXML/jackson + +Wiremock +* License: Apache 2.0 License +* Project: https://github.com/wiremock/wiremock +* Source: https://github.com/wiremock/wiremock diff --git a/bundles/org.openhab.binding.lgthinq/README.md b/bundles/org.openhab.binding.lgthinq/README.md new file mode 100644 index 0000000000000..bdb41962e2ab1 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/README.md @@ -0,0 +1,200 @@ +# LG ThinQ Binding + +This binding was developed to integrate the LG ThinQ API with openHAB. + +## Supported Things + +This binding support several devices from the LG ThinQ Devices V1 & V2 line. +See the table bellow: + +| Thing ID | Device Name | Versions | Special Functions | Commands | Obs | +|---------------------|-----------------|----------|------------------------------|-------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| air-conditioner-401 | Air Conditioner | V1 & V2 | Filter and Energy Monitoring | All features in LG App, except Wind Direction | | +| dishwasher-204 | Dish Washer | V2 | None | None | Provide only some channels to follow the cycle | +| 222 | Dryer Tower | V1 & V2 | None | All features in LG App (including remote start) | LG has a WasherDryer Tower that is 2 in one device.
When this device is discovered by this binding, it's recognized as 2 separated devices Washer and Dryer | +| washer-tower-221 | Washer Tower | V1 & V2 | None | All features in LG App (including remote start) | LG has a WasherDryer Tower that is 2 in one device.
When this device is discovered by this binding, it's recognized as 2 separated devices Washer and Dryer | +| washer-201 | Washer Machine | V1 & V2 | None | All features in LG App (including remote start) | | +| dryer-tower-222 | Dryer Machine | V1 & V2 | None | All features in LG App (including remote start) | | +| fridge-101 | Refrigerator | V1 & V2 | None | All features in LG App | | +| heatpump-401HP | Heat Pump | V1 & V2 | None | All features in LG App | | + +## `bridge` Thing + +This binding has a Bridge responsible for discovering and registering LG Things. +Thus, adding the Bridge (LGThinq GW Bridge) is the first step in configuring this Binding. +The following parameters are available to configure the Bridge and to link to your LG Account as well: + +| Bridge Parameter | Label | Description | Obs | +|--------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| language | User Language | More frequent languages used | If you choose other, you can fill Manual user language (only if your language was not pre-defined in this combo | +| country | User Country | More frequent countries used | If you choose other, you can fill Manual user country (only if your country was not pre-defined in this combo | +| manualLanguage | Manual User Language | The acronym for the language (PT, EN, IT, etc) | | +| manualCountry | Manual User Country | The acronym for the country (UK, US, BR, etc) | | +| username | LG User name | The LG user's account (normally an email) | | +| password | LG Password | The LG user's password | | +| pollingIntervalSec | Polling Discovery Interval | It the time (in seconds) that the bridge wait to try to fetch de devices registered to the user's account and, if find some new device, will show available to link. Please, choose some long time greater than 300 seconds | +| alternativeServer | Alt Gateway Server | Only used if you have some proxy to the LG API Server or for Mock Tests | | + +## Discovery + +This Binding has auto-discovery for the supported LG Thinq devices. +Once LG Thinq Bridge has been added, LG Thinq devices linked to your account will be automatically discovered and displayed in the OpenHab Inbox. + +## Thing Configuration + +All the configurations are pre-defined by the discovery process. +But you can customize to fine-tune the device's state polling process. +See the table below: + +| Parameter | Description | Default Value | Supported Devices | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|-------------------------------| +| pollingPeriodPowerOffSeconds | Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. | 10 | All | +| pollingPeriodPowerOnSeconds | Seconds to wait to the next polling for device state (dashboard channels) | 10 | All | +| pollingExtraInfoPeriodSeconds | Seconds to wait to the next polling for Device's Extra Info (energy consumption, remaining filter, etc) | 60 | Air Conditioner and Heat Pump | +| pollExtraInfoOnPowerOff | If enables, extra info will be fetched in the polling process even when the device is powered off. It's not so common, since extra info are normally changed only when the device is running. | Off | Air Conditioner and Heat Pump | + +## Channels + +### Air Conditioner + +Most, but not all, LG ThinQ Air Conditioners support the following channels: + +#### Dashboard Channels + +| channel # | channel | type | description | +|---------------------|--------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| target-temperature | Target Temperature | Number:Temperature | Defines the desired target temperature for the device | +| current-temperature | Temperature | Number:Temperature | Read-Only channel that indicates the current temperature informed by the device | +| fan-speed | Fan Speed | Number | This channel let you choose the current label value for the fan speed (Low, Medium, High, Auto, etc.). These values are pre-configured in discovery time. | +| op-mode | Operation Mode | Number (Labeled) | Defines device's operation mode (Fan, Cool, Dry, etc). These values are pre-configured at discovery time. | +| power | Power | Switch | Define the device's Current Power state. | +| cool-jet | Cool Jet | Switch | Switch Cool Jet ON/OFF | +| auto-dry | Auto Dry | Switch | Switch Auto Dry ON/OFF | +| energy-saving | Energy Saving | Switch | Switch Energy Saving ON/OFF | +| fan-step-up-down | Fan VDir | Number | Define the fan vertical direction's mode (Off, Upper, Circular, Up, Middle Up, etc) | +| fan-step-left-right | Fan HDir | Number | Define the fan horizontal direction's mode (Off, Lefter, Left, Circular, Right, etc) | + +#### More Information Channel + +| channel # | channel | type | description | +|----------------------|--------------------------------|----------------------|------------------------------------------------------------------------------| +| extra-info-collector | Enable Extended Info Collector | Switch | Enable/Disable the extra information collector to update the bellow channels | +| current-energy | Current Energy | Number:Energy | The Current Energy consumption in Kwh | +| remaining-filter | Remaining Filter | Number:Dimensionless | Percentage of the remaining filter | + +### Heat Pump + +LG ThinQ Heat Pump supports the following channels + +#### Dashboard Channels + +| channel # | channel | type | description | +|---------------------|---------------------|--------------------|-----------------------------------------------------------------------------------------------------------| +| target-temperature | Target Temperature | Number:Temperature | Defines the desired target temperature for the device | +| min-temperature | Minimum Temperature | Number:Temperature | Minimum temperature for the current operation mode | +| max-temperature | Maximum Temperature | Number:Temperature | Maximum temperature for the current operation mode | +| current-temperature | Temperature | Number:Temperature | Read-Only channel that indicates the current temperature informed by the device | +| op-mode | Operation Mode | Number (Labeled) | Defines device's operation mode (Fan, Cool, Dry, etc). These values are pre-configured at discovery time. | +| power | Power | Switch | Define the device's Current Power state. | +| air-water-switch | Air/Water Switch | Switch | Switch the heat pump operation between Air or Water | + +#### More Information Channel + +| channel # | channel | type | description | +|----------------------|--------------------------------|----------------------|------------------------------------------------------------------------------| +| extra-info-collector | Enable Extended Info Collector | Switch | Enable/Disable the extra information collector to update the bellow channels | +| current-energy | Current Energy | Number:Energy | The Current Energy consumption in Kwh | + +### Washer Machine + +LG ThinQ Washer Machine supports the following channels + +#### Dashboard Channels + +| channel # | channel | type | description | +|-------------------|-------------------|------------|------------------------------------------------------------------------------------------------------------| +| state | Washer State | String | General State of the Washer | +| power | Power | Switch | Define the device's Current Power state. | +| process-state | Process State | String | States of the running cycle | +| course | Course | String | Course set up to work | +| temperature-level | Temperature Level | String | Temperature level supported by the Washer (Cold, 20, 30, 40, 50, etc.) | +| door-lock | Door Lock | Switch | Display if the Door is Locked. | +| rinse | Rinse | String | The Rinse set program | +| spin | Spin | String | The Spin set option | +| delay-time | Delay Time | String | Delay time programmed to start the cycle | +| remain-time | Remaining Time | String | Remaining time to finish the course | +| stand-by | Stand By Mode | Switch | If the Washer is in stand-by-mode | +| remote-start-flag | Remote Start | Switch | If the Washer is in remote start mode waiting to be remotely started | + +#### Remote Start Option + +This Channel Group is only available if the Washer is configured to Remote Start + +| channel # | channel | type | description | +|----------------------|-------------------|--------------------|---------------------------------------------------------------------------------------------------------| +| rs-start-stop | Remote Start/Stop | Switch | Switch to control if you want to start/stop the cycle remotely | +| rs-course | Course to Run | String (Selection) | The pre-programmed course (or default) is shown. You can change-it if you want before remote start | +| rs-temperature-level | Temperature Level | String (Selection) | The pre-programmed temperature (or default) is shown. You can change-it if you want before remote start | +| rs-spin | Spin | String | The pre-programmed spin (or default) is shown. You can change-it if you want before remote start | +| rs-rinse | Rinse | String | The pre-programmed rinse (or default) is shown. You can change-it if you want before remote start | + +### Dryer Machine + +LG ThinQ Dryer Machine supports the following channels + +#### Dashboard Channels + +| channel # | channel | type | description | +|-------------------|-------------------|---------|------------------------------------------------------------------------| +| power | Power | Switch | Define the device's Current Power state. | +| state | Dryer State | String | General State of the Washer | +| process-state | Process State | String | States of the running cycle | +| course | Course | String | Course set up to work | +| temperature-level | Temperature Level | String | Temperature level supported by the Washer (Cold, 20, 30, 40, 50, etc.) | +| child-lock | Child Lock | Switch | Display if the Door is Locked. | +| dry-level | Dry Level Course | String | Dry level set to work in the course | +| delay-time | Delay Time | String | Delay time programmed to start the cycle | +| remain-time | Remaining Time | String | Remaining time to finish the course | +| stand-by | Stand By Mode | Switch | If the Washer is in stand-by-mode | +| remote-start-flag | Remote Start | Switch | If the Washer is in remote start mode waiting to be remotely started | + +#### Remote Start Option + +This Channel Group is only available if the Dryer is configured to Remote Start + +| channel # | channel | type | description | +|---------------|-------------------|--------------------|---------------------------------------------------------------------------------------------------------| +| rs-start-stop | Remote Start/Stop | Switch | Switch to control if you want to start/stop the cycle remotely | +| rs-course | Course to Run | String (Selection) | The pre-programmed course (or default) is shown. You can change-it if you want before remote start | + +### Dryer/Washer Tower + +LG ThinQ Dryer/Washer is recognized as 2 different things: Dryer & Washer machines. Thus, for this device, follow the sessions for Dryer Machine and Washer Machine + +### Refrigerator + +LG ThinQ Refrigerator supports the following channels + +#### Dashboard Channels + +| channel # | channel | type | description | +|----------------------|-------------------------------|--------------------|--------------------------------------------------------------------------------| +| some-door-open | Door Open | Contact | Advice if the door is opened | +| freezer-temperature | Freezer Set Point Temperature | Number:Temperature | Temperature level chosen. This channel supports commands to change temperature | +| fridge-temperature | Fridge Set Point Temperature | Number:Temperature | Temperature level chosen. This channel supports commands to change temperature | +| temp-unit | Temp. Unit | String | Temperature Unit (°C/F). Supports command to change the unit | +| fr-express-mode | Express Freeze | Switch | Channel to change the express freeze function (ON/OFF/Rapid) | +| fr-express-cool-mode | Express Cool | Switch | Channel to switch ON/OFF express cool function | +| fr-eco-friendly-mode | Vacation | Switch | Channel to switch ON/OFF Vacation function (unit will work in eco mode) | + +#### More Information + +This Channel Group is reports useful information data for the device: + +| channel # | channel | type | description | +|---------------------|------------------|-----------|------------------------------------------------------------| +| fr-fresh-air-filter | Fresh Air Filter | String | Shows the Fresh Air filter status (OFF/AUTO/POWER/REPLACE) | +| fr-water-filter | Water Filter | String | Shows the filter's used months | + +OBS: some versions of this device can not support all the channels, depending on the model's capabilities. + diff --git a/bundles/org.openhab.binding.lgthinq/doc/bridge-configuration.jpg b/bundles/org.openhab.binding.lgthinq/doc/bridge-configuration.jpg new file mode 100644 index 0000000000000..80f2587d33947 Binary files /dev/null and b/bundles/org.openhab.binding.lgthinq/doc/bridge-configuration.jpg differ diff --git a/bundles/org.openhab.binding.lgthinq/doc/lg-thinq-air.jpg b/bundles/org.openhab.binding.lgthinq/doc/lg-thinq-air.jpg new file mode 100644 index 0000000000000..ad3e8912b8a21 Binary files /dev/null and b/bundles/org.openhab.binding.lgthinq/doc/lg-thinq-air.jpg differ diff --git a/bundles/org.openhab.binding.lgthinq/pom.xml b/bundles/org.openhab.binding.lgthinq/pom.xml new file mode 100644 index 0000000000000..285e19d71825b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.lgthinq + + openHAB Add-ons :: Bundles :: LG Thinq Binding + + + + com.github.tomakehurst + wiremock-jre8 + 2.32.0 + test + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/feature/feature.xml b/bundles/org.openhab.binding.lgthinq/src/main/feature/feature.xml new file mode 100644 index 0000000000000..7c617dee5c6f1 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.lgthinq/${project.version} + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQBindingConstants.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQBindingConstants.java new file mode 100644 index 0000000000000..42b091ee39fbe --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQBindingConstants.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal; + +import java.io.File; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.LGServicesConstants; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.core.OpenHAB; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link LGThinQBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQBindingConstants extends LGServicesConstants { + + public static final String BINDING_ID = "lgthinq"; + + // =============== Thing Type IDs ================== + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final ThingTypeUID THING_TYPE_AIR_CONDITIONER = new ThingTypeUID(BINDING_ID, + DeviceTypes.AIR_CONDITIONER.thingTypeId()); + public static final ThingTypeUID THING_TYPE_WASHING_MACHINE = new ThingTypeUID(BINDING_ID, + DeviceTypes.WASHERDRYER_MACHINE.thingTypeId()); + public static final ThingTypeUID THING_TYPE_WASHING_TOWER = new ThingTypeUID(BINDING_ID, + DeviceTypes.WASHER_TOWER.thingTypeId()); + public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, DeviceTypes.DRYER.thingTypeId()); + public static final ThingTypeUID THING_TYPE_HEAT_PUMP = new ThingTypeUID(BINDING_ID, + DeviceTypes.HEAT_PUMP.thingTypeId()); + public static final ThingTypeUID THING_TYPE_DRYER_TOWER = new ThingTypeUID(BINDING_ID, + DeviceTypes.DRYER_TOWER.thingTypeId()); + + public static final ThingTypeUID THING_TYPE_FRIDGE = new ThingTypeUID(BINDING_ID, DeviceTypes.FRIDGE.thingTypeId()); + public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, + DeviceTypes.DISH_WASHER.thingTypeId()); + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_CONDITIONER, + THING_TYPE_WASHING_MACHINE, THING_TYPE_WASHING_TOWER, THING_TYPE_DRYER_TOWER, THING_TYPE_DRYER, + THING_TYPE_FRIDGE, THING_TYPE_BRIDGE, THING_TYPE_HEAT_PUMP, THING_TYPE_DISHWASHER); + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_AIR_CONDITIONER, + THING_TYPE_WASHING_MACHINE, THING_TYPE_WASHING_TOWER, THING_TYPE_DRYER, THING_TYPE_DRYER_TOWER, + THING_TYPE_HEAT_PUMP); + + // ======== Common Channels & Constants ======== + public static final String CHANNEL_DASHBOARD_GRP_ID = "dashboard"; + public static final String CHANNEL_EXTENDED_INFO_GRP_ID = "extended-information"; + public static final String CHANNEL_EXTENDED_INFO_COLLECTOR_ID = "extra-info-collector"; + // Max number of retries trying to get the monitor (V1) until consider ERROR in the connection + public static final int MAX_GET_MONITOR_RETRIES = 3; + public static final int DISCOVERY_SEARCH_TIMEOUT = 20; + // === Biding property info + public static final String PROP_INFO_DEVICE_ALIAS = "device-alias"; + public static final String PROP_INFO_DEVICE_ID = "device-id"; + public static final String PROP_INFO_MODEL_URL_INFO = "model-url-info"; + public static final String PROP_INFO_PLATFORM_TYPE = "platform-type"; + // === UserData Directory and File Format + public static String THINQ_USER_DATA_FOLDER = OpenHAB.getUserDataFolder() + File.separator + "thinq"; + public static String THINQ_CONNECTION_DATA_FILE = THINQ_USER_DATA_FOLDER + File.separator + "thinqbridge-%s.json"; + public static String BASE_CAP_CONFIG_DATA_FILE = THINQ_USER_DATA_FOLDER + File.separator + "thinq-%s-cap.json"; + + // ==================================================== + + /** + * ============ Air Conditioner Channels & Constant Definition ============= + */ + public static final String CHANNEL_AC_AIR_CLEAN_ID = "air-clean"; + public static final String CHANNEL_AC_AIR_WATER_SWITCH_ID = "air-water-switch"; + public static final String CHANNEL_AC_AUTO_DRY_ID = "auto-dry"; + public static final String CHANNEL_AC_COOL_JET_ID = "cool-jet"; + public static final String CHANNEL_AC_CURRENT_ENERGY_ID = "current-energy"; + public static final String CHANNEL_AC_CURRENT_TEMP_ID = "current-temperature"; + public static final String CHANNEL_AC_ENERGY_SAVING_ID = "energy-saving"; + public static final String CHANNEL_AC_FAN_SPEED_ID = "fan-speed"; + public static final String CHANNEL_AC_MAX_TEMP_ID = "max_temperature"; + public static final String CHANNEL_AC_MIN_TEMP_ID = "min_temperature"; + public static final String CHANNEL_AC_MOD_OP_ID = "op-mode"; + public static final String CHANNEL_AC_POWER_ID = "power"; + public static final String CHANNEL_AC_REMAINING_FILTER_ID = "remaining-filter"; + public static final String CHANNEL_AC_STEP_LEFT_RIGHT_ID = "fan-step-left-right"; + public static final String CHANNEL_AC_STEP_UP_DOWN_ID = "fan-step-up-down"; + public static final String CHANNEL_AC_TARGET_TEMP_ID = "target-temperature"; + + /** + * ============ Fridge's Channels & Constant Definition ============= + */ + public static final String CHANNEL_FR_ACTIVE_SAVING = "fr-active-saving"; + public static final String CHANNEL_FR_DOOR_OPEN = "fr-some-door-open"; + public static final String CHANNEL_FR_EXPRESS_COOL_MODE = "fr-express-cool-mode"; + public static final String CHANNEL_FR_EXPRESS_FREEZE_MODE = "fr-express-mode"; + public static final String CHANNEL_FR_FREEZER_TEMP_ID = "fr-freezer-temperature"; + public static final String CHANNEL_FR_FRESH_AIR_FILTER = "fr-fresh-air-filter"; + public static final String CHANNEL_FR_FRIDGE_TEMP_ID = "fr-fridge-temperature"; + public static final String CHANNEL_FR_ICE_PLUS = "fr-ice-plus"; + public static final String CHANNEL_FR_REF_TEMP_UNIT = "fr-temp-unit"; + public static final String CHANNEL_FR_SMART_SAVING_MODE_V2 = "fr-smart-saving-mode"; + public static final String CHANNEL_FR_SMART_SAVING_SWITCH_V1 = "fr-smart-saving-switch"; + public static final String CHANNEL_FR_VACATION_MODE = "fr-eco-friendly-mode"; + public static final String CHANNEL_FR_WATER_FILTER = "fr-water-filter"; + + /** + * ============ Washing Machine/Dryer and DishWasher Channels & Constant Definition ============= + * DishWasher, Washing Machine and Dryer have the same channel core and features + */ + public static final String CHANNEL_WMD_CHILD_LOCK_ID = "child-lock"; + public static final String CHANNEL_WMD_DRY_LEVEL_ID = "dry-level"; + public static final String CHANNEL_WMD_COURSE_ID = "course"; + public static final String CHANNEL_WMD_DELAY_TIME_ID = "delay-time"; + public static final String CHANNEL_WMD_DOOR_LOCK_ID = "door-lock"; + public static final String CHANNEL_WMD_PROCESS_STATE_ID = "process-state"; + public static final String CHANNEL_WMD_REMAIN_TIME_ID = "remain-time"; + public static final String CHANNEL_WMD_REMOTE_COURSE = "rs-course"; + public static final String CHANNEL_WMD_REMOTE_START_GRP_ID = "rs-grp"; + public static final String CHANNEL_WMD_REMOTE_START_ID = "rs-flag"; + public static final String CHANNEL_WMD_REMOTE_START_START_STOP = "rs-start-stop"; + public static final String CHANNEL_WMD_RINSE_ID = "rinse"; + public static final String CHANNEL_WMD_SMART_COURSE_ID = "smart-course"; + public static final String CHANNEL_WMD_SPIN_ID = "spin"; + public static final String CHANNEL_WMD_STAND_BY_ID = "stand-by"; + public static final String CHANNEL_WMD_STATE_ID = "state"; + public static final String CHANNEL_WMD_TEMP_LEVEL_ID = "temperature-level"; + public static final String CHANNEL_WMD_REMOTE_START_RINSE = "rs-rinse"; + public static final String CHANNEL_WMD_REMOTE_START_SPIN = "rs-spin"; + public static final String CHANNEL_WMD_REMOTE_START_TEMP = "rs-temperature-level"; + + // ============================================================================== + // DIGEST CONSTANTS + public static final String MESSAGE_DIGEST_ALGORITHM = "SHA-512"; + public static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQBridgeConfiguration.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQBridgeConfiguration.java new file mode 100644 index 0000000000000..024a83bcfd0e7 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQBridgeConfiguration.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGThinQBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQBridgeConfiguration { + /** + * Sample configuration parameters. Replace with your own. + */ + public String username = ""; + public String password = ""; + public String country = ""; + public String language = ""; + public String manualCountry = ""; + public String manualLanguage = ""; + public int pollingIntervalSec = 0; + public String alternativeServer = ""; + + public LGThinQBridgeConfiguration() { + } + + public LGThinQBridgeConfiguration(String username, String password, String country, String language, + int pollingIntervalSec, String alternativeServer) { + this.username = username; + this.password = password; + this.country = country; + this.language = language; + this.pollingIntervalSec = pollingIntervalSec; + this.alternativeServer = alternativeServer; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getCountry() { + if ("--".equals(country)) { + return manualCountry; + } + return country; + } + + public String getLanguage() { + if ("--".equals(language)) { + return manualLanguage; + } + return language; + } + + public int getPollingIntervalSec() { + return pollingIntervalSec; + } + + public String getAlternativeServer() { + return alternativeServer; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQHandlerFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQHandlerFactory.java new file mode 100644 index 0000000000000..197ffc82c553a --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQHandlerFactory.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_AIR_CONDITIONER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_BRIDGE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_DISHWASHER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_DRYER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_DRYER_TOWER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_FRIDGE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_HEAT_PUMP; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_MACHINE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_TOWER; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.internal.handler.LGThinQAirConditionerHandler; +import org.openhab.binding.lgthinq.internal.handler.LGThinQBridgeHandler; +import org.openhab.binding.lgthinq.internal.handler.LGThinQDishWasherHandler; +import org.openhab.binding.lgthinq.internal.handler.LGThinQFridgeHandler; +import org.openhab.binding.lgthinq.internal.handler.LGThinQWasherDryerHandler; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinQHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.lgthinq") +public class LGThinQHandlerFactory extends BaseThingHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(LGThinQHandlerFactory.class); + + private final HttpClientFactory httpClientFactory; + + private final LGThinQStateDescriptionProvider stateDescriptionProvider; + + @Nullable + @Reference + protected ItemChannelLinkRegistry itemChannelLinkRegistry; + + @Activate + public LGThinQHandlerFactory(final @Reference LGThinQStateDescriptionProvider stateDescriptionProvider, + @Reference final HttpClientFactory httpClientFactory) { + this.stateDescriptionProvider = stateDescriptionProvider; + this.httpClientFactory = httpClientFactory; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return LGThinQBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (THING_TYPE_AIR_CONDITIONER.equals(thingTypeUID) || THING_TYPE_HEAT_PUMP.equals(thingTypeUID)) { + return new LGThinQAirConditionerHandler(thing, stateDescriptionProvider, + Objects.requireNonNull(itemChannelLinkRegistry), httpClientFactory); + } else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new LGThinQBridgeHandler((Bridge) thing, httpClientFactory); + } else if (THING_TYPE_WASHING_MACHINE.equals(thingTypeUID) || THING_TYPE_WASHING_TOWER.equals(thingTypeUID)) { + return new LGThinQWasherDryerHandler(thing, stateDescriptionProvider, + Objects.requireNonNull(itemChannelLinkRegistry), httpClientFactory); + } else if (THING_TYPE_DRYER.equals(thingTypeUID) || THING_TYPE_DRYER_TOWER.equals(thingTypeUID)) { + return new LGThinQWasherDryerHandler(thing, stateDescriptionProvider, + Objects.requireNonNull(itemChannelLinkRegistry), httpClientFactory); + } else if (THING_TYPE_FRIDGE.equals(thingTypeUID)) { + return new LGThinQFridgeHandler(thing, stateDescriptionProvider, + Objects.requireNonNull(itemChannelLinkRegistry), httpClientFactory); + } else if (THING_TYPE_DISHWASHER.equals(thingTypeUID)) { + return new LGThinQDishWasherHandler(thing, stateDescriptionProvider, + Objects.requireNonNull(itemChannelLinkRegistry), httpClientFactory); + } + logger.warn("Thing not supported by this Factory: {}", thingTypeUID.getId()); + return null; + } + + @Override + public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, + @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) { + if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return super.createThing(thingTypeUID, configuration, thingUID, null); + } else if (THING_TYPE_AIR_CONDITIONER.equals(thingTypeUID) || THING_TYPE_HEAT_PUMP.equals(thingTypeUID)) { + return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID); + } else if (THING_TYPE_WASHING_MACHINE.equals(thingTypeUID) || THING_TYPE_WASHING_TOWER.equals(thingTypeUID)) { + return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID); + } else if (THING_TYPE_DRYER.equals(thingTypeUID) || THING_TYPE_DRYER_TOWER.equals(thingTypeUID)) { + return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID); + } else if (THING_TYPE_FRIDGE.equals(thingTypeUID)) { + return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID); + } else if (THING_TYPE_DISHWASHER.equals(thingTypeUID)) { + return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQStateDescriptionProvider.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQStateDescriptionProvider.java new file mode 100644 index 0000000000000..d7bee71d7ac5e --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/LGThinQStateDescriptionProvider.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link LGThinQStateDescriptionProvider} Custom State Description Provider + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@Component(service = { DynamicStateDescriptionProvider.class, LGThinQStateDescriptionProvider.class }) +public class LGThinQStateDescriptionProvider extends BaseDynamicStateDescriptionProvider { + @Activate + public LGThinQStateDescriptionProvider(final @Reference EventPublisher eventPublisher, // + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, // + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/discovery/LGThinqDiscoveryService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/discovery/LGThinqDiscoveryService.java new file mode 100644 index 0000000000000..b7dcd1d162419 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/discovery/LGThinqDiscoveryService.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.discovery; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.DISCOVERY_SEARCH_TIMEOUT; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_DEVICE_ALIAS; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_DEVICE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_MODEL_URL_INFO; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_PLATFORM_TYPE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.SUPPORTED_THING_TYPES; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_AIR_CONDITIONER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_DISHWASHER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_DRYER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_DRYER_TOWER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_FRIDGE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_HEAT_PUMP; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_MACHINE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_TOWER; +import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.internal.handler.LGThinQBridgeHandler; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory.LGThinQGeneralApiClientService; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.LGDevice; +import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinqDiscoveryService} - Responsable to discovery new LG Thinq Devices for the registered Bridge + * + * @author Nemer Daud - Initial contribution + */ +@Component(scope = ServiceScope.PROTOTYPE, service = LGThinqDiscoveryService.class) +@NonNullByDefault +public class LGThinqDiscoveryService extends AbstractThingHandlerDiscoveryService { + + private final Logger logger = LoggerFactory.getLogger(LGThinqDiscoveryService.class); + private @Nullable ThingUID bridgeHandlerUID; + private @Nullable LGThinQGeneralApiClientService lgApiClientService; + + public LGThinqDiscoveryService() { + super(LGThinQBridgeHandler.class, SUPPORTED_THING_TYPES, DISCOVERY_SEARCH_TIMEOUT); + } + + @Override + public void initialize() { + bridgeHandlerUID = thingHandler.getThing().getUID(); + // thingHandler is the LGThinQBridgeHandler + thingHandler.registerDiscoveryListener(this); + lgApiClientService = LGThinQApiClientServiceFactory + .newGeneralApiClientService(thingHandler.getHttpClientFactory()); + super.initialize(); + } + + @Override + protected void startScan() { + logger.debug("Scan started"); + // thingHandler is the LGThinQBridgeHandler + thingHandler.runDiscovery(); + } + + @Override + protected synchronized void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan(), thingHandler.getThing().getUID()); + } + + public void removeLgDeviceDiscovery(LGDevice device) { + logger.debug("Thing removed from discovery: {}", device.getDeviceId()); + try { + ThingUID thingUID = getThingUID(device); + thingRemoved(thingUID); + } catch (LGThinqException e) { + logger.warn("Error getting Thing UID"); + } + } + + public void addLgDeviceDiscovery(LGDevice device) { + logger.debug("Thing added to discovery: {}", device.getDeviceId()); + String modelId = device.getModelName(); + ThingUID thingUID; + ThingTypeUID thingTypeUID; + try { + // load capability to cache and troubleshooting + Objects.requireNonNull(lgApiClientService, "Unexpected null here") + .loadDeviceCapability(device.getDeviceId(), device.getModelJsonUri(), false); + thingUID = getThingUID(device); + thingTypeUID = getThingTypeUID(device); + } catch (LGThinqException e) { + logger.debug("Discovered unsupported LG device of type '{}'({}) and model '{}' with id {}", + device.getDeviceType(), device.getDeviceTypeId(), modelId, device.getDeviceId()); + return; + } + + Map properties = new HashMap<>(); + properties.put(PROP_INFO_DEVICE_ID, device.getDeviceId()); + properties.put(PROP_INFO_DEVICE_ALIAS, device.getAlias()); + properties.put(PROP_INFO_MODEL_URL_INFO, device.getModelJsonUri()); + properties.put(PROP_INFO_PLATFORM_TYPE, device.getPlatformType()); + properties.put(PROPERTY_MODEL_ID, modelId); + + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID) + .withProperties(properties).withBridge(bridgeHandlerUID).withRepresentationProperty(PROP_INFO_DEVICE_ID) + .withLabel(device.getAlias()).build(); + + thingDiscovered(discoveryResult); + } + + private ThingUID getThingUID(LGDevice device) throws LGThinqException { + ThingTypeUID thingTypeUID = getThingTypeUID(device); + return new ThingUID(thingTypeUID, + Objects.requireNonNull(bridgeHandlerUID, "bridgeHandleUid should never be null here"), + device.getDeviceId()); + } + + private ThingTypeUID getThingTypeUID(LGDevice device) throws LGThinqException { + // Short switch, but is here because it is going to be increase after new LG Devices were added + return switch (device.getDeviceType()) { + case AIR_CONDITIONER -> THING_TYPE_AIR_CONDITIONER; + case HEAT_PUMP -> THING_TYPE_HEAT_PUMP; + case WASHERDRYER_MACHINE -> THING_TYPE_WASHING_MACHINE; + case WASHER_TOWER -> THING_TYPE_WASHING_TOWER; + case DRYER_TOWER -> THING_TYPE_DRYER_TOWER; + case DRYER -> THING_TYPE_DRYER; + case FRIDGE -> THING_TYPE_FRIDGE; + case DISH_WASHER -> THING_TYPE_DISHWASHER; + default -> + throw new LGThinqException(String.format("device type [%s] not supported", device.getDeviceType())); + }; + } + + @Override + public void dispose() { + super.dispose(); + removeOlderResults(Instant.now().toEpochMilli(), bridgeHandlerUID); + thingHandler.unregisterDiscoveryListener(); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/BaseThingWithExtraInfoHandler.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/BaseThingWithExtraInfoHandler.java new file mode 100644 index 0000000000000..7f1ab4972cb6c --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/BaseThingWithExtraInfoHandler.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.BaseThingHandler; + +/** + * The {@link BaseThingWithExtraInfoHandler} contains method definitions to the Handle be able to work + * with extra info data. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class BaseThingWithExtraInfoHandler extends BaseThingHandler { + /** + * Creates a new instance of this class for the {@link Thing}. + * + * @param thing the thing that should be handled, not null + */ + public BaseThingWithExtraInfoHandler(Thing thing) { + super(thing); + } + + /** + * Handle must implement this method to update device's extra information collected to the respective channels. + * + * @param energyStateAttributes map containing the key and values collected + */ + protected void updateExtraInfoStateChannels(Map energyStateAttributes) { + throw new UnsupportedOperationException( + "Method must be implemented in the Handle that supports energy collector. It most likely a bug"); + } + + /** + * Must be implemented with the code to get energy state if the thing supports it. + * + * @return map containing energy state attributes + */ + protected Map collectExtraInfoState() throws LGThinqException { + throw new UnsupportedOperationException( + "Method must be implemented in the Handle that supports energy collector. It most likely a bug"); + } + + /** + * Reset (put in UNDEF) the channels related to extra information. Normally called when the collector stops. + */ + protected void resetExtraInfoChannels() { + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQAbstractDeviceHandler.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQAbstractDeviceHandler.java new file mode 100644 index 0000000000000..40e91caee1390 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQAbstractDeviceHandler.java @@ -0,0 +1,817 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.BINDING_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_POWER_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_EXTENDED_INFO_COLLECTOR_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.MAX_GET_MONITOR_RETRIES; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_DEVICE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_MODEL_URL_INFO; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_PLATFORM_TYPE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_PLATFORM_TYPE_V2; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.internal.LGThinQStateDescriptionProvider; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientService; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqAccessException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiExhaustionException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqDeviceV1MonitorExpiredException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqDeviceV1OfflineException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDataType; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; +import org.openhab.binding.lgthinq.lgservices.model.SnapshotDefinition; +import org.openhab.core.items.Item; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinQAbstractDeviceHandler} is a main interface contract for all LG Thinq things + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class LGThinQAbstractDeviceHandler<@NonNull C extends CapabilityDefinition, @NonNull S extends SnapshotDefinition> + extends BaseThingWithExtraInfoHandler { + private final Logger logger = LoggerFactory.getLogger(LGThinQAbstractDeviceHandler.class); + protected @Nullable LGThinQBridgeHandler account; + protected final String lgPlatformType; + private S lastShot; + protected final ItemChannelLinkRegistry itemChannelLinkRegistry; + @Nullable + protected C thinQCapability; + private @Nullable Future commandExecutorQueueJob; + private final ExecutorService executorService = Executors.newFixedThreadPool(1); + private @Nullable ScheduledFuture thingStatePollingJob; + private @Nullable ScheduledFuture extraInfoCollectorPollingJob; + private final ScheduledExecutorService pollingScheduler = Executors.newScheduledThreadPool(1); + /** Defined in the configurations of the thing. */ + private int pollingPeriodOnSeconds = 10; + private int pollingPeriodOffSeconds = 10; + private int currentPeriodSeconds = 10; + private int pollingExtraInfoPeriodSeconds = 60; + private boolean pollExtraInfoOnPowerOff = false; + private Integer fetchMonitorRetries = 0; + private boolean monitorV1Began = false; + private boolean isThingReconfigured = false; + private String monitorWorkId = ""; + protected final LinkedBlockingQueue commandBlockQueue = new LinkedBlockingQueue<>(30); + private String bridgeId = ""; + private ThingStatus lastThingStatus = ThingStatus.UNKNOWN; + // Bridges status that this thing must top scanning for state change + private static final Set BRIDGE_STATUS_DETAIL_ERROR = Set.of(ThingStatusDetail.BRIDGE_OFFLINE, + ThingStatusDetail.BRIDGE_UNINITIALIZED, ThingStatusDetail.COMMUNICATION_ERROR, + ThingStatusDetail.CONFIGURATION_ERROR); + + protected final LGThinQStateDescriptionProvider stateDescriptionProvider; + + protected S getLastShot() { + return Objects.requireNonNull(lastShot, "LastShot shouldn't be null. It most likely a bug."); + } + + @SuppressWarnings("unchecked") + public Class getSnapshotClass() { + return (Class) (Objects.requireNonNull((ParameterizedType) getClass().getGenericSuperclass(), + "Unexpected null here")).getActualTypeArguments()[1]; + } + + @SuppressWarnings("null") + public LGThinQAbstractDeviceHandler(Thing thing, LGThinQStateDescriptionProvider stateDescriptionProvider, + ItemChannelLinkRegistry itemChannelLinkRegistry) { + super(thing); + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.stateDescriptionProvider = stateDescriptionProvider; + normalizeConfigurationsAndProperties(); + lgPlatformType = String.valueOf(thing.getProperties().get(PROP_INFO_PLATFORM_TYPE)); + + Class snapshotClass = getSnapshotClass(); + try { + this.lastShot = snapshotClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Snapshot class can't be instantiated. It most likely a bug", e); + } + } + + private LGThinQBridgeHandler getAccountBridgeHandler() { + return Objects.requireNonNull(this.account, "BridgeHandler not initialized. It most likely a bug"); + } + + private void normalizeConfigurationsAndProperties() { + List.of(PROP_INFO_PLATFORM_TYPE, PROP_INFO_MODEL_URL_INFO, PROP_INFO_DEVICE_ID).forEach(p -> { + if (!thing.getProperties().containsKey(p)) { + thing.setProperty(p, (String) thing.getConfiguration().get(p)); + } + }); + } + + protected static class AsyncCommandParams { + final String channelUID; + final Command command; + + public AsyncCommandParams(String channelUUID, Command command) { + this.channelUID = channelUUID; + this.command = command; + } + } + + /** + * Returns the simple channel UID name, i.e., without group. + * + * @param uid Full UID name + * @return simple channel UID name, i.e., without group. + */ + protected String getSimpleChannelUID(String uid) { + String simpleChannelUID; + if (uid.indexOf("#") > 0) { + // I have to remove the channelGroup from de channelUID + simpleChannelUID = uid.split("#")[1]; + } else { + simpleChannelUID = uid; + } + return simpleChannelUID; + } + + /** + * Return empty string if null argument is passed + * + * @param value value to test + * @return empty string if null argument is passed + */ + protected final String emptyIfNull(@Nullable String value) { + return Objects.requireNonNullElse(value, ""); + } + + /** + * Return the key informed if there is no correpondent value in map for that key. + * + * @param map map with key/value + * @param key key to search for a value into map + * @return return value related to that key in the map, or the own key if there is no correspondent. + */ + protected final String keyIfValueNotFound(Map map, String key) { + return Objects.requireNonNullElse(map.get(key), key); + } + + @SuppressWarnings("null") + protected void startCommandExecutorQueueJob() { + if (commandExecutorQueueJob == null || commandExecutorQueueJob.isDone()) { + commandExecutorQueueJob = getExecutorService().submit(getQueuedCommandExecutor()); + } + } + + @SuppressWarnings("null") + protected void stopCommandExecutorQueueJob() { + if (commandExecutorQueueJob != null) { + commandExecutorQueueJob.cancel(true); + } + commandExecutorQueueJob = null; + } + + @SuppressWarnings("null") + protected void handleStatusChanged(ThingStatus newStatus, ThingStatusDetail statusDetail) { + if (lastThingStatus != ThingStatus.ONLINE && newStatus == ThingStatus.ONLINE) { + // start the thing polling + startThingStatePolling(); + } else if (lastThingStatus == ThingStatus.ONLINE && newStatus == ThingStatus.OFFLINE + && BRIDGE_STATUS_DETAIL_ERROR.contains(statusDetail)) { + // comunication error is not a specific Bridge error, then we must analise it to give + // this thinq the change to recovery from communication errors + if (statusDetail != ThingStatusDetail.COMMUNICATION_ERROR + || (getBridge() != null && getBridge().getStatus() != ThingStatus.ONLINE)) { + // in case of status offline, I only stop the polling if is not an COMMUNICATION_ERROR or if + // the bridge is out + stopThingStatePolling(); + stopExtraInfoCollectorPolling(); + } + + } + lastThingStatus = newStatus; + } + + @Override + protected void updateStatus(ThingStatus newStatus, ThingStatusDetail statusDetail, @Nullable String description) { + handleStatusChanged(newStatus, statusDetail); + super.updateStatus(newStatus, statusDetail, description); + } + + @Override + @SuppressWarnings("null") + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateThingStateFromLG(); + } else { + AsyncCommandParams params = new AsyncCommandParams(channelUID.getId(), command); + try { + // Ensure commands are send in a pipe per device. + commandBlockQueue.add(params); + } catch (IllegalStateException ex) { + getLogger().warn( + "Device's command queue reached the size limit. Probably the device is busy ou stuck. Ignoring command."); + getLogger().debug("Status of the commandQueue: consumer: {}, size: {}", + commandExecutorQueueJob == null || commandExecutorQueueJob.isDone() ? "OFF" : "ON", + commandBlockQueue.size()); + if (logger.isTraceEnabled()) { + // logging the thread dump to analise possible stuck thread. + ThreadMXBean bean = ManagementFactory.getThreadMXBean(); + ThreadInfo[] infos = bean.dumpAllThreads(true, true); + String message = ""; + for (ThreadInfo i : infos) { + message = String.format("%s\n%s", message, i.toString()); + } + logger.trace("{}", message); + } + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.COMMUNICATION_ERROR, + "Device Command Queue is Busy"); + } + + } + } + + protected ExecutorService getExecutorService() { + return executorService; + } + + public String getDeviceId() { + return Objects.requireNonNullElse(getThing().getProperties().get(PROP_INFO_DEVICE_ID), "undef"); + } + + public abstract String getDeviceAlias(); + + public abstract String getDeviceUriJsonConfig(); + + public abstract void onDeviceRemoved(); + + public abstract void onDeviceDisconnected(); + + public abstract void updateChannelDynStateDescription() throws LGThinqApiException; + + public abstract LGThinQApiClientService<@NonNull C, @NonNull S> getLgThinQAPIClientService(); + + public C getCapabilities() throws LGThinqApiException { + if (thinQCapability == null) { + thinQCapability = getLgThinQAPIClientService().getCapability(getDeviceId(), getDeviceUriJsonConfig(), + false); + } + return Objects.requireNonNull(thinQCapability, "Unexpected error. Return of capability shouldn't ever be null"); + } + + /** + * Get the first item value associated to the channel + * + * @param channelUID channel + * @return value of the first item related to this channel. + */ + @Nullable + protected String getItemLinkedValue(ChannelUID channelUID) { + Set items = itemChannelLinkRegistry.getLinkedItems(channelUID); + if (!items.isEmpty()) { + for (Item i : items) { + return i.getState().toString(); + } + } + return null; + } + + protected abstract Logger getLogger(); + + @Override + public void initialize() { + getLogger().debug("Initializing Thinq thing."); + + Bridge bridge = getBridge(); + if (bridge != null) { + this.account = (LGThinQBridgeHandler) bridge.getHandler(); + this.bridgeId = bridge.getUID().getId(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge not set"); + return; + } + + initializeThing(bridge.getStatus()); + } + + protected void initializeThing(@Nullable ThingStatus bridgeStatus) { + getLogger().debug("initializeThing LQ Thinq {}. Bridge status {}", getThing().getUID(), bridgeStatus); + String thingId = getThing().getUID().getId(); + + // setup configurations + loadConfigurations(); + + if (!thingId.isBlank()) { + try { + updateChannelDynStateDescription(); + } catch (LGThinqApiException e) { + getLogger().error( + "Error updating channels dynamic options descriptions based on capabilities of the device. Fallback to default values.", + e); + } + // registry this thing to the bridge + if (account == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Account not set"); + } else { + getAccountBridgeHandler().registryListenerThing(this); + if (bridgeStatus == null) { + updateStatus(ThingStatus.UNINITIALIZED); + } else { + switch (bridgeStatus) { + case ONLINE: + updateStatus(ThingStatus.ONLINE); + break; + case INITIALIZING: + case UNINITIALIZED: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + break; + case UNKNOWN: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + break; + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + break; + } + } + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-no-device-id"); + } + // finally, start command queue, regardless of the thing state, since we can still try to send commands without + // property ONLINE (the successful result from command request can put the thing in ONLINE status). + startCommandExecutorQueueJob(); + if (getThing().getStatus() == ThingStatus.ONLINE) { + try { + getLgThinQAPIClientService().initializeDevice(bridgeId, getDeviceId()); + } catch (Exception e) { + logger.warn("Error initializing the device {} from bridge {}.", thingId, bridgeId, e); + } + // force start state pooling if the device is ONLINE + resetExtraInfoChannels(); + startThingStatePolling(); + } + } + + public void refreshStatus() { + if (thing.getStatus() == ThingStatus.OFFLINE) { + initialize(); + } + } + + private void loadConfigurations() { + isThingReconfigured = true; + if (getThing().getConfiguration().containsKey("pollingPeriodPowerOnSeconds")) { + pollingPeriodOnSeconds = ((BigDecimal) getThing().getConfiguration().get("pollingPeriodPowerOnSeconds")) + .intValue(); + } + if (getThing().getConfiguration().containsKey("pollingPeriodPowerOffSeconds")) { + pollingPeriodOffSeconds = ((BigDecimal) getThing().getConfiguration().get("pollingPeriodPowerOffSeconds")) + .intValue(); + } + if (getThing().getConfiguration().containsKey("pollingExtraInfoPeriodSeconds")) { + pollingExtraInfoPeriodSeconds = ((BigDecimal) getThing().getConfiguration() + .get("pollingExtraInfoPeriodSeconds")).intValue(); + } + if (getThing().getConfiguration().containsKey("pollExtraInfoOnPowerOff")) { + pollExtraInfoOnPowerOff = (Boolean) getThing().getConfiguration().get("pollExtraInfoOnPowerOff"); + } + // if the periods are the same, I can define currentPeriod for polling right now. If not, I postpone to the nest + // snapshot update + if (pollingPeriodOffSeconds == pollingPeriodOnSeconds) { + currentPeriodSeconds = pollingPeriodOffSeconds; + } + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + getLogger().debug("bridgeStatusChanged {}", bridgeStatusInfo); + // restart scheduler + initializeThing(bridgeStatusInfo.getStatus()); + } + + /** + * Determine if the Handle for this device supports Energy State Collector + * + * @return always false and must be overridden if the implemented handler supports energy collector + */ + protected boolean isExtraInfoCollectorSupported() { + return false; + } + + /** + * Returns if the energy collector is enabled. The handle that supports energy collection must + * provide a logic that defines if the collector is currently enabled. Normally, it uses a Switch Channel + * to provide a way to the user turn on/off the collector. + * + * @return true if the energyCollector must be enabled. + */ + protected boolean isExtraInfoCollectorEnabled() { + return false; + } + + private class UpdateExtraInfoCollector implements Runnable { + @Override + public void run() { + updateExtraInfoState(); + } + } + + private void updateExtraInfoState() { + if (!isExtraInfoCollectorSupported()) { + logger.error( + "The Energy Collector was started for a Handler that doesn't support it. It most likely a bug."); + return; + } + try { + Map extraInfoCollected = collectExtraInfoState(); + updateExtraInfoStateChannels(extraInfoCollected); + } catch (LGThinqException ex) { + getLogger().error( + "Error getting energy state and update the correlated channels. DeviceName: {}, DeviceId: {}. Error: {}", + getDeviceAlias(), getDeviceId(), ex.getMessage(), ex); + } + } + + private class UpdateThingStateFromLG implements Runnable { + @Override + public void run() { + updateThingStateFromLG(); + } + } + + protected void updateThingStateFromLG() { + try { + @Nullable + S shot = getSnapshotDeviceAdapter(getDeviceId()); + if (shot == null) { + // no data to update. Maybe, the monitor stopped, then it's going to be restarted next try. + return; + } + fetchMonitorRetries = 0; + if (!shot.isOnline()) { + if (getThing().getStatus() != ThingStatus.OFFLINE) { + // only update channels if the device has just gone OFFLINE. + updateDeviceChannelsWrapper(shot); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.device-disconnected"); + onDeviceDisconnected(); + } + } else { + // do not update channels if the device is offline + updateDeviceChannelsWrapper(shot); + if (getThing().getStatus() != ThingStatus.ONLINE) + updateStatus(ThingStatus.ONLINE); + } + + } catch (LGThinqAccessException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (LGThinqApiExhaustionException e) { + fetchMonitorRetries++; + getLogger().warn("LG API returns null monitoring data for the thing {}/{}. No data available yet ?", + getDeviceAlias(), getDeviceId()); + if (fetchMonitorRetries > MAX_GET_MONITOR_RETRIES) { + getLogger().error( + "The thing {}/{} reach maximum retries for monitor data. Thing goes OFFLINE until next retry.", + getDeviceAlias(), getDeviceId(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } catch (LGThinqException e) { + getLogger().error("Error updating thing {}/{} from LG API. Thing goes OFFLINE until next retry: {}", + getDeviceAlias(), getDeviceId(), e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (Exception e) { + getLogger().error( + "System error in pooling thread (UpdateDevice) for device {}/{}. Filtering to do not stop the thread", + getDeviceAlias(), getDeviceId(), e); + } + } + + private void handlePowerChange(@Nullable DevicePowerState previous, DevicePowerState current) { + // isThingReconfigured is true when configurations has been updated or thing has just initialized + // this will force to analyse polling periods and starts + if (!isThingReconfigured && previous == current) { + // no changes needed + return; + } + + // change from OFF to ON / OFF to ON + boolean isEnableToStartCollector = isExtraInfoCollectorEnabled() && isExtraInfoCollectorSupported(); + + if (current == DevicePowerState.DV_POWER_ON) { + currentPeriodSeconds = pollingPeriodOnSeconds; + + // if extendedInfo collector is enabled, then force do start to prevent previous stop + if (isEnableToStartCollector) { + startExtraInfoCollectorPolling(); + } + } else { + currentPeriodSeconds = pollingPeriodOffSeconds; + + // if it's configured to stop extra-info collection on PowerOff, then stop the job + if (!pollExtraInfoOnPowerOff) { + stopExtraInfoCollectorPolling(); + } else if (isEnableToStartCollector) { + startExtraInfoCollectorPolling(); + } + } + + // restart thing state polling for the new poolingPeriod configuration + if (pollingPeriodOffSeconds != pollingPeriodOnSeconds) { + stopThingStatePolling(); + } + + startThingStatePolling(); + } + + private void updateDeviceChannelsWrapper(S snapshot) throws LGThinqApiException { + updateDeviceChannels(snapshot); + // handle power changes + handlePowerChange(getLastShot().getPowerStatus(), snapshot.getPowerStatus()); + // after updated successfully, copy snapshot to last snapshot + lastShot = snapshot; + // and finish the cycle of thing reconfiguration (when thing starts or has configurations changed - if it's the + // case) + isThingReconfigured = false; + } + + protected abstract void updateDeviceChannels(S snapshot) throws LGThinqApiException; + + protected String translateFeatureToItemType(FeatureDataType dataType) { + return switch (dataType) { + case UNDEF, ENUM -> "String"; + case RANGE -> "Dimmer"; + case BOOLEAN -> "Switch"; + default -> throw new IllegalStateException( + String.format("Feature DataType %s not supported for this ThingHandler", dataType)); + }; + } + + @SuppressWarnings("null") + protected void stopThingStatePolling() { + if (!(thingStatePollingJob == null || thingStatePollingJob.isDone())) { + getLogger().debug("Stopping LG thinq polling for device/alias: {}/{}", getDeviceId(), getDeviceAlias()); + thingStatePollingJob.cancel(true); + } + thingStatePollingJob = null; + } + + @SuppressWarnings("null") + private void stopExtraInfoCollectorPolling() { + if (extraInfoCollectorPollingJob != null && !extraInfoCollectorPollingJob.isDone()) { + getLogger().debug("Stopping Energy Collector for device/alias: {}/{}", getDeviceId(), getDeviceAlias()); + extraInfoCollectorPollingJob.cancel(true); + } + resetExtraInfoChannels(); + extraInfoCollectorPollingJob = null; + } + + @SuppressWarnings("null") + protected void startThingStatePolling() { + if (thingStatePollingJob == null || thingStatePollingJob.isDone()) { + getLogger().debug("Starting LG thinq polling for device/alias: {}/{}", getDeviceId(), getDeviceAlias()); + thingStatePollingJob = pollingScheduler.scheduleWithFixedDelay(new UpdateThingStateFromLG(), 5, + currentPeriodSeconds, TimeUnit.SECONDS); + } + } + + /** + * Method responsible for start the Energy Collector Polling. Must be called buy the handles when it's desired. + * Normally, the thing has a Switch Channel that enable/disable the energy collector. By default, the collector is + * disabled. + */ + @SuppressWarnings("null") + private void startExtraInfoCollectorPolling() { + if (extraInfoCollectorPollingJob == null || extraInfoCollectorPollingJob.isDone()) { + getLogger().debug("Starting Energy Collector for device/alias: {}/{}", getDeviceId(), getDeviceAlias()); + extraInfoCollectorPollingJob = pollingScheduler.scheduleWithFixedDelay(new UpdateExtraInfoCollector(), 10, + pollingExtraInfoPeriodSeconds, TimeUnit.SECONDS); + } + } + + private void stopDeviceV1Monitor(String deviceId) { + try { + monitorV1Began = false; + getLgThinQAPIClientService().stopMonitor(getBridgeId(), deviceId, monitorWorkId); + } catch (LGThinqDeviceV1OfflineException e) { + getLogger().debug("Monitor stopped. Device is unavailable/disconnected", e); + } catch (Exception e) { + getLogger().error("Error stopping LG Device monitor", e); + } + } + + protected String getBridgeId() { + if (bridgeId.isBlank() && getBridge() == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); + getLogger().error("Configuration error um Thinq Thing - No Bridge defined for the thing."); + return "UNKNOWN"; + } else if (bridgeId.isBlank() && getBridge() != null) { + bridgeId = Objects.requireNonNull(getBridge()).getUID().getId(); + } + return bridgeId; + } + + abstract protected DeviceTypes getDeviceType(); + + @Nullable + protected S getSnapshotDeviceAdapter(String deviceId) throws LGThinqApiException, LGThinqApiExhaustionException { + // analise de platform version + if (LG_API_PLATFORM_TYPE_V2.equals(lgPlatformType)) { + return getLgThinQAPIClientService().getDeviceData(getBridgeId(), getDeviceId(), getCapabilities()); + } else { + try { + if (!monitorV1Began) { + monitorWorkId = getLgThinQAPIClientService().startMonitor(getBridgeId(), getDeviceId()); + monitorV1Began = true; + } + } catch (LGThinqDeviceV1OfflineException e) { + try { + stopDeviceV1Monitor(deviceId); + } catch (Exception ignored) { + } + return getLgThinQAPIClientService().buildDefaultOfflineSnapshot(); + } catch (Exception e) { + stopDeviceV1Monitor(deviceId); + throw new LGThinqApiException("Error starting device monitor in LG API for the device:" + deviceId, e); + } + int retries = 10; + @Nullable + S shot; + try { + while (retries > 0) { + // try to get monitoring data result 3 times. + + shot = getLgThinQAPIClientService().getMonitorData(getBridgeId(), deviceId, monitorWorkId, + getDeviceType(), getCapabilities()); + if (shot != null) { + return shot; + } + Thread.sleep(500); + retries--; + + } + } catch (LGThinqDeviceV1MonitorExpiredException | LGThinqUnmarshallException e) { + getLogger().debug("Monitor for device {} is invalid. Forcing stop and start to next cycle.", deviceId); + return null; + } catch (Exception e) { + // If it can't get monitor handler, then stop monitor and restart the process again in new + // interaction + // Force restart monitoring because of the errors returned (just in case) + throw new LGThinqApiException("Error getting monitor data for the device:" + deviceId, e); + } finally { + try { + stopDeviceV1Monitor(deviceId); + } catch (Exception ignored) { + } + } + throw new LGThinqApiExhaustionException("Exhausted trying to get monitor data for the device:" + deviceId); + } + } + + protected abstract void processCommand(AsyncCommandParams params) throws LGThinqApiException; + + protected Runnable getQueuedCommandExecutor() { + return queuedCommandExecutor; + } + + private final Runnable queuedCommandExecutor = () -> { + while (true) { + AsyncCommandParams params; + try { + params = commandBlockQueue.take(); + } catch (InterruptedException e) { + getLogger().debug("Interrupting async command queue executor."); + return; + } + + try { + processCommand(params); + String channelUid = getSimpleChannelUID(params.channelUID); + if (CHANNEL_AC_POWER_ID.equals(channelUid)) { + // if processed command come from POWER channel, then force updateDeviceChannels immediatly + // this is importante to analise if the poolings need to be changed in time. + updateThingStateFromLG(); + } else if (CHANNEL_EXTENDED_INFO_COLLECTOR_ID.equals(channelUid)) { + if (OnOffType.ON.equals(params.command)) { + logger.debug("Turning ON extended information collector"); + if (pollExtraInfoOnPowerOff + || DevicePowerState.DV_POWER_ON.equals(getLastShot().getPowerStatus())) { + startExtraInfoCollectorPolling(); + } + } else if (OnOffType.OFF.equals(params.command)) { + logger.debug("Turning OFF extended information collector"); + stopExtraInfoCollectorPolling(); + } else { + logger.error("Command {} for {} channel is unexpected. It's most likely a bug", params.command, + CHANNEL_EXTENDED_INFO_COLLECTOR_ID); + } + } + } catch (LGThinqException e) { + getLogger().error("Error executing Command {} to the channel {}. Thing goes offline until retry", + params.command, params.channelUID, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (Exception e) { + getLogger().error("System error executing Command {} to the channel {}. Ignoring command", + params.command, params.channelUID, e); + } + } + }; + + @Override + public void dispose() { + logger.debug("Disposing Thinq Thing {}", getDeviceId()); + + if (account != null) { + getAccountBridgeHandler().unRegistryListenerThing(this); + } + + stopThingStatePolling(); + stopExtraInfoCollectorPolling(); + stopCommandExecutorQueueJob(); + try { + if (LGAPIVerion.V1_0.equals(getCapabilities().getDeviceVersion())) { + stopDeviceV1Monitor(getDeviceId()); + } + } catch (Exception e) { + logger.warn("Can't stop active monitor. It's can be normally ignored. Cause:{}", e.getMessage()); + } + } + + /** + * Create Dynamic channel. The channel type must be pre-defined in the thing definition (xml) and with + * the same name as the channel. + * + * @param channelNameAndTypeName channel name to be created and the same channel type name defined in the channels + * descriptor + * @param channelUuid Uid of the channel + * @param itemType item type (see openhab documentation) + * @return return the new channel created + */ + protected Channel createDynChannel(String channelNameAndTypeName, ChannelUID channelUuid, String itemType) { + if (getCallback() == null) { + throw new IllegalStateException("Unexpected behaviour. Callback not ready! Can't create dynamic channels"); + } else { + // dynamic create channel + ChannelBuilder builder = Objects + .requireNonNull(getCallback(), "Not expected callback null here. It most likely a bug") + .createChannelBuilder(channelUuid, new ChannelTypeUID(BINDING_ID, channelNameAndTypeName)); + Channel channel = builder.withKind(ChannelKind.STATE).withAcceptedItemType(itemType).build(); + updateThing(editThing().withChannel(channel).build()); + return channel; + } + } + + protected void manageDynChannel(ChannelUID channelUid, String channelName, String itemType, + boolean isFeatureAvailable) { + Channel chan = getThing().getChannel(channelUid); + if (chan == null && isFeatureAvailable) { + createDynChannel(channelName, channelUid, itemType); + } else if (chan != null && (!isFeatureAvailable)) { + updateThing(editThing().withoutChannel(chan.getUID()).build()); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQAirConditionerHandler.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQAirConditionerHandler.java new file mode 100644 index 0000000000000..1f0786b5b45d1 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQAirConditionerHandler.java @@ -0,0 +1,502 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CAP_EXTRA_ATTR_FILTER_MAX_TIME_TO_USE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CAP_EXTRA_ATTR_FILTER_USED_TIME; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CAP_EXTRA_ATTR_INSTANT_POWER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_AIR_CLEAN_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_AIR_WATER_SWITCH_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_AUTO_DRY_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_COOL_JET_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_CURRENT_ENERGY_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_CURRENT_TEMP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_ENERGY_SAVING_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_FAN_SPEED_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_MAX_TEMP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_MIN_TEMP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_MOD_OP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_POWER_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_REMAINING_FILTER_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_STEP_LEFT_RIGHT_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_STEP_UP_DOWN_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_TARGET_TEMP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_DASHBOARD_GRP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_EXTENDED_INFO_COLLECTOR_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_EXTENDED_INFO_GRP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_DEVICE_ALIAS; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_MODEL_URL_INFO; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_AIR_CONDITIONER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_HEAT_PUMP; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_ACHP_OP_MODE_COOL_KEY; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_ACHP_OP_MODE_HEAT_KEY; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_FAN_SPEED; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_OP_MODE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_STEP_LEFT_RIGHT_MODE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_STEP_UP_DOWN_MODE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_HP_AIR_SWITCH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_HP_WATER_SWITCH; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.internal.LGThinQStateDescriptionProvider; +import org.openhab.binding.lgthinq.lgservices.LGThinQACApiClientService; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientService; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACTargetTmp; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ExtendedDeviceInfo; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.types.Command; +import org.openhab.core.types.StateOption; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link LGThinQAirConditionerHandler} Handle Air Conditioner and HeatPump Things + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQAirConditionerHandler extends LGThinQAbstractDeviceHandler { + + public final ChannelGroupUID channelGroupExtendedInfoUID; + public final ChannelGroupUID channelGroupDashboardUID; + private final ChannelUID powerChannelUID; + private final ChannelUID opModeChannelUID; + private final ChannelUID hpAirWaterSwitchChannelUID; + private final ChannelUID fanSpeedChannelUID; + private final ChannelUID targetTempChannelUID; + private final ChannelUID currTempChannelUID; + private final ChannelUID minTempChannelUID; + private final ChannelUID maxTempChannelUID; + private final ChannelUID jetModeChannelUID; + private final ChannelUID airCleanChannelUID; + private final ChannelUID autoDryChannelUID; + private final ChannelUID stepUpDownChannelUID; + private final ChannelUID stepLeftRightChannelUID; + private final ChannelUID energySavingChannelUID; + private final ChannelUID extendedInfoCollectorChannelUID; + private final ChannelUID currentEnergyConsumptionChannelUID; + private final ChannelUID remainingFilterChannelUID; + + private double minTempConstraint = 16, maxTempConstraint = 30; + private final ObjectMapper mapper = new ObjectMapper(); + private final Logger logger = LoggerFactory.getLogger(LGThinQAirConditionerHandler.class); + private final LGThinQACApiClientService lgThinqACApiClientService; + + public LGThinQAirConditionerHandler(Thing thing, LGThinQStateDescriptionProvider stateDescriptionProvider, + ItemChannelLinkRegistry itemChannelLinkRegistry, HttpClientFactory httpClientFactory) { + super(thing, stateDescriptionProvider, itemChannelLinkRegistry); + lgThinqACApiClientService = LGThinQApiClientServiceFactory.newACApiClientService(lgPlatformType, + httpClientFactory); + channelGroupDashboardUID = new ChannelGroupUID(getThing().getUID(), CHANNEL_DASHBOARD_GRP_ID); + channelGroupExtendedInfoUID = new ChannelGroupUID(getThing().getUID(), CHANNEL_EXTENDED_INFO_GRP_ID); + + opModeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_MOD_OP_ID); + hpAirWaterSwitchChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_AIR_WATER_SWITCH_ID); + targetTempChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_TARGET_TEMP_ID); + minTempChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_MIN_TEMP_ID); + maxTempChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_MAX_TEMP_ID); + currTempChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_CURRENT_TEMP_ID); + fanSpeedChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_FAN_SPEED_ID); + jetModeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_COOL_JET_ID); + airCleanChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_AIR_CLEAN_ID); + autoDryChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_AUTO_DRY_ID); + energySavingChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_ENERGY_SAVING_ID); + stepUpDownChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_STEP_UP_DOWN_ID); + stepLeftRightChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_STEP_LEFT_RIGHT_ID); + powerChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_AC_POWER_ID); + extendedInfoCollectorChannelUID = new ChannelUID(channelGroupExtendedInfoUID, + CHANNEL_EXTENDED_INFO_COLLECTOR_ID); + currentEnergyConsumptionChannelUID = new ChannelUID(channelGroupExtendedInfoUID, CHANNEL_AC_CURRENT_ENERGY_ID); + remainingFilterChannelUID = new ChannelUID(channelGroupExtendedInfoUID, CHANNEL_AC_REMAINING_FILTER_ID); + } + + @Override + public void initialize() { + super.initialize(); + try { + ACCapability cap = getCapabilities(); + if (!isExtraInfoCollectorSupported()) { + ThingBuilder builder = editThing() + .withoutChannels(this.getThing().getChannelsOfGroup(channelGroupExtendedInfoUID.getId())); + updateThing(builder.build()); + } else if (!cap.isEnergyMonitorAvailable()) { + ThingBuilder builder = editThing().withoutChannel(currentEnergyConsumptionChannelUID); + updateThing(builder.build()); + } else if (!cap.isFilterMonitorAvailable()) { + ThingBuilder builder = editThing().withoutChannel(remainingFilterChannelUID); + updateThing(builder.build()); + } + } catch (LGThinqApiException e) { + logger.warn("Error getting capability of the device: {}", getDeviceId()); + } + } + + @Override + protected void updateDeviceChannels(ACCanonicalSnapshot shot) { + updateState(powerChannelUID, + DevicePowerState.DV_POWER_ON.equals(shot.getPowerStatus()) ? OnOffType.ON : OnOffType.OFF); + updateState(opModeChannelUID, new DecimalType(shot.getOperationMode())); + if (DeviceTypes.HEAT_PUMP.equals(getDeviceType())) { + updateState(hpAirWaterSwitchChannelUID, new DecimalType(shot.getHpAirWaterTempSwitch())); + } + updateState(fanSpeedChannelUID, new DecimalType(shot.getAirWindStrength())); + updateState(currTempChannelUID, new DecimalType(shot.getCurrentTemperature())); + updateState(targetTempChannelUID, new DecimalType(shot.getTargetTemperature())); + try { + ACCapability acCap = getCapabilities(); + if (getThing().getChannel(stepUpDownChannelUID) != null) { + updateState(stepUpDownChannelUID, new DecimalType((int) shot.getStepUpDownMode())); + } + if (getThing().getChannel(stepLeftRightChannelUID) != null) { + updateState(stepLeftRightChannelUID, new DecimalType((int) shot.getStepLeftRightMode())); + } + if (getThing().getChannel(jetModeChannelUID) != null) { + Double commandCoolJetOn = Double.valueOf(acCap.getCoolJetModeCommandOn()); + updateState(jetModeChannelUID, + commandCoolJetOn.equals(shot.getCoolJetMode()) ? OnOffType.ON : OnOffType.OFF); + } + if (getThing().getChannel(airCleanChannelUID) != null) { + Double commandAirCleanOn = Double.valueOf(acCap.getAirCleanModeCommandOn()); + updateState(airCleanChannelUID, + commandAirCleanOn.equals(shot.getAirCleanMode()) ? OnOffType.ON : OnOffType.OFF); + } + if (getThing().getChannel(energySavingChannelUID) != null) { + Double energySavingOn = Double.valueOf(acCap.getEnergySavingModeCommandOn()); + updateState(energySavingChannelUID, + energySavingOn.equals(shot.getEnergySavingMode()) ? OnOffType.ON : OnOffType.OFF); + } + if (getThing().getChannel(autoDryChannelUID) != null) { + Double autoDryOn = Double.valueOf(acCap.getCoolJetModeCommandOn()); + updateState(autoDryChannelUID, autoDryOn.equals(shot.getAutoDryMode()) ? OnOffType.ON : OnOffType.OFF); + } + if (DeviceTypes.HEAT_PUMP.equals(getDeviceType())) { + // HP has different combination of min and max target temperature depending on the switch mode and + // operation + // mode + String opModeValue = Objects + .requireNonNullElse(acCap.getOpMode().get(getLastShot().getOperationMode().toString()), ""); + if (CAP_HP_AIR_SWITCH.equals(shot.getHpAirWaterTempSwitch())) { + if (opModeValue.equals(CAP_ACHP_OP_MODE_COOL_KEY)) { + minTempConstraint = shot.getHpAirTempCoolMin(); + maxTempConstraint = shot.getHpAirTempCoolMax(); + } else if (opModeValue.equals(CAP_ACHP_OP_MODE_HEAT_KEY)) { + minTempConstraint = shot.getHpAirTempHeatMin(); + maxTempConstraint = shot.getHpAirTempHeatMax(); + } + } else if (CAP_HP_WATER_SWITCH.equals(shot.getHpAirWaterTempSwitch())) { + if (opModeValue.equals(CAP_ACHP_OP_MODE_COOL_KEY)) { + minTempConstraint = shot.getHpWaterTempCoolMin(); + maxTempConstraint = shot.getHpWaterTempCoolMax(); + } else if (opModeValue.equals(CAP_ACHP_OP_MODE_HEAT_KEY)) { + minTempConstraint = shot.getHpWaterTempHeatMin(); + maxTempConstraint = shot.getHpWaterTempHeatMax(); + } + } else { + logger.warn("Invalid value received by HP snapshot for the air/water switch property: {}", + shot.getHpAirWaterTempSwitch()); + } + updateState(minTempChannelUID, new DecimalType(BigDecimal.valueOf(minTempConstraint))); + updateState(maxTempChannelUID, new DecimalType(BigDecimal.valueOf(maxTempConstraint))); + } + + } catch (LGThinqApiException e) { + logger.error("Unexpected Error getting ACCapability Capabilities", e); + } catch (NumberFormatException e) { + logger.warn("command value for capability is not numeric.", e); + } + } + + @Override + public void updateChannelDynStateDescription() throws LGThinqApiException { + ACCapability acCap = getCapabilities(); + manageDynChannel(jetModeChannelUID, CHANNEL_AC_COOL_JET_ID, "Switch", acCap.isJetModeAvailable()); + manageDynChannel(autoDryChannelUID, CHANNEL_AC_AUTO_DRY_ID, "Switch", acCap.isAutoDryModeAvailable()); + manageDynChannel(airCleanChannelUID, CHANNEL_AC_AIR_CLEAN_ID, "Switch", acCap.isAirCleanAvailable()); + manageDynChannel(energySavingChannelUID, CHANNEL_AC_ENERGY_SAVING_ID, "Switch", + acCap.isEnergySavingAvailable()); + manageDynChannel(stepUpDownChannelUID, CHANNEL_AC_STEP_UP_DOWN_ID, "Number", acCap.isStepUpDownAvailable()); + manageDynChannel(stepLeftRightChannelUID, CHANNEL_AC_STEP_LEFT_RIGHT_ID, "Number", + acCap.isStepLeftRightAvailable()); + manageDynChannel(stepLeftRightChannelUID, CHANNEL_AC_STEP_LEFT_RIGHT_ID, "Number", + acCap.isStepLeftRightAvailable()); + + if (!acCap.getFanSpeed().isEmpty()) { + List options = new ArrayList<>(); + acCap.getFanSpeed() + .forEach((k, v) -> options.add(new StateOption(k, emptyIfNull(CAP_AC_FAN_SPEED.get(v))))); + stateDescriptionProvider.setStateOptions(fanSpeedChannelUID, options); + } + if (!acCap.getOpMode().isEmpty()) { + List options = new ArrayList<>(); + acCap.getOpMode().forEach((k, v) -> options.add(new StateOption(k, emptyIfNull(CAP_AC_OP_MODE.get(v))))); + stateDescriptionProvider.setStateOptions(opModeChannelUID, options); + } + if (!acCap.getStepLeftRight().isEmpty()) { + List options = new ArrayList<>(); + acCap.getStepLeftRight().forEach( + (k, v) -> options.add(new StateOption(k, emptyIfNull(CAP_AC_STEP_LEFT_RIGHT_MODE.get(v))))); + stateDescriptionProvider.setStateOptions(stepLeftRightChannelUID, options); + } + if (!acCap.getStepUpDown().isEmpty()) { + List options = new ArrayList<>(); + acCap.getStepUpDown() + .forEach((k, v) -> options.add(new StateOption(k, emptyIfNull(CAP_AC_STEP_UP_DOWN_MODE.get(v))))); + stateDescriptionProvider.setStateOptions(stepUpDownChannelUID, options); + } + } + + @Override + public LGThinQApiClientService getLgThinQAPIClientService() { + return lgThinqACApiClientService; + } + + @Override + protected Logger getLogger() { + return logger; + } + + protected DeviceTypes getDeviceType() { + if (THING_TYPE_HEAT_PUMP.equals(getThing().getThingTypeUID())) { + return DeviceTypes.HEAT_PUMP; + } else if (THING_TYPE_AIR_CONDITIONER.equals(getThing().getThingTypeUID())) { + return DeviceTypes.AIR_CONDITIONER; + } else { + throw new IllegalArgumentException( + "DeviceTypeUuid [" + getThing().getThingTypeUID() + "] not expected for AirConditioner/HeatPump"); + } + } + + @Override + public String getDeviceAlias() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_DEVICE_ALIAS)); + } + + @Override + public String getDeviceUriJsonConfig() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_MODEL_URL_INFO)); + } + + @Override + public void onDeviceRemoved() { + } + + @Override + public void onDeviceDisconnected() { + } + + protected void resetExtraInfoChannels() { + updateState(currentEnergyConsumptionChannelUID, UnDefType.UNDEF); + if (!isExtraInfoCollectorEnabled()) { // if collector is enabled we can keep the current value + updateState(remainingFilterChannelUID, UnDefType.UNDEF); + } + } + + protected void processCommand(AsyncCommandParams params) throws LGThinqApiException { + Command command = params.command; + switch (getSimpleChannelUID(params.channelUID)) { + case CHANNEL_AC_MOD_OP_ID: { + if (params.command instanceof DecimalType) { + lgThinqACApiClientService.changeOperationMode(getBridgeId(), getDeviceId(), + ((DecimalType) command).intValue()); + } else { + logger.warn("Received command different of Numeric in Mod Operation. Ignoring"); + } + break; + } + case CHANNEL_AC_FAN_SPEED_ID: { + if (command instanceof DecimalType) { + lgThinqACApiClientService.changeFanSpeed(getBridgeId(), getDeviceId(), + ((DecimalType) command).intValue()); + } else { + logger.warn("Received command different of Numeric in FanSpeed Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_STEP_UP_DOWN_ID: { + if (command instanceof DecimalType) { + lgThinqACApiClientService.changeStepUpDown(getBridgeId(), getDeviceId(), getLastShot(), + ((DecimalType) command).intValue()); + } else { + logger.warn("Received command different of Numeric in Step Up/Down Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_STEP_LEFT_RIGHT_ID: { + if (command instanceof DecimalType) { + lgThinqACApiClientService.changeStepLeftRight(getBridgeId(), getDeviceId(), getLastShot(), + ((DecimalType) command).intValue()); + } else { + logger.warn("Received command different of Numeric in Step Left/Right Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_POWER_ID: { + if (command instanceof OnOffType) { + lgThinqACApiClientService.turnDevicePower(getBridgeId(), getDeviceId(), + command == OnOffType.ON ? DevicePowerState.DV_POWER_ON : DevicePowerState.DV_POWER_OFF); + } else { + logger.warn("Received command different of OnOffType in Power Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_COOL_JET_ID: { + if (command instanceof OnOffType) { + lgThinqACApiClientService.turnCoolJetMode(getBridgeId(), getDeviceId(), + command == OnOffType.ON ? getCapabilities().getCoolJetModeCommandOn() + : getCapabilities().getCoolJetModeCommandOff()); + } else { + logger.warn("Received command different of OnOffType in CoolJet Mode Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_AIR_CLEAN_ID: { + if (command instanceof OnOffType) { + lgThinqACApiClientService.turnAirCleanMode(getBridgeId(), getDeviceId(), + command == OnOffType.ON ? getCapabilities().getAirCleanModeCommandOn() + : getCapabilities().getAirCleanModeCommandOff()); + } else { + logger.warn("Received command different of OnOffType in AirClean Mode Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_AUTO_DRY_ID: { + if (command instanceof OnOffType) { + lgThinqACApiClientService.turnAutoDryMode(getBridgeId(), getDeviceId(), + command == OnOffType.ON ? getCapabilities().getAutoDryModeCommandOn() + : getCapabilities().getAutoDryModeCommandOff()); + } else { + logger.warn("Received command different of OnOffType in AutoDry Mode Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_ENERGY_SAVING_ID: { + if (command instanceof OnOffType) { + lgThinqACApiClientService.turnEnergySavingMode(getBridgeId(), getDeviceId(), + command == OnOffType.ON ? getCapabilities().getEnergySavingModeCommandOn() + : getCapabilities().getEnergySavingModeCommandOff()); + } else { + logger.warn("Received command different of OnOffType in EvergySaving Mode Channel. Ignoring"); + } + break; + } + case CHANNEL_AC_TARGET_TEMP_ID: { + double targetTemp; + if (command instanceof DecimalType) { + targetTemp = ((DecimalType) command).doubleValue(); + } else if (command instanceof QuantityType) { + targetTemp = ((QuantityType) command).doubleValue(); + } else { + logger.warn("Received command different of Numeric in TargetTemp Channel. Ignoring"); + break; + } + // analise temperature constraints + if (targetTemp > maxTempConstraint || targetTemp < minTempConstraint) { + // values out of range + logger.warn("Target Temperature: {} is out of range: {} - {}. Ignoring command", targetTemp, + minTempConstraint, maxTempConstraint); + break; + } + lgThinqACApiClientService.changeTargetTemperature(getBridgeId(), getDeviceId(), + ACTargetTmp.statusOf(targetTemp)); + break; + } + case CHANNEL_EXTENDED_INFO_COLLECTOR_ID: { + break; + } + default: { + logger.warn("Command {} to the channel {} not supported. Ignored.", command, params.channelUID); + } + } + } + // =========== Energy Colletor Implementation ============= + + @Override + protected boolean isExtraInfoCollectorSupported() { + try { + return getCapabilities().isEnergyMonitorAvailable() || getCapabilities().isFilterMonitorAvailable(); + } catch (LGThinqApiException e) { + logger.warn("Can't get capabilities of the device: {}", getDeviceId()); + } + return false; + } + + @Override + protected boolean isExtraInfoCollectorEnabled() { + return OnOffType.ON.toString().equals(getItemLinkedValue(extendedInfoCollectorChannelUID)); + } + + @Override + @SuppressWarnings("null") + protected Map collectExtraInfoState() throws LGThinqException { + ExtendedDeviceInfo info = lgThinqACApiClientService.getExtendedDeviceInfo(getBridgeId(), getDeviceId()); + return mapper.convertValue(info, new TypeReference<>() { + }); + } + + @Override + protected void updateExtraInfoStateChannels(Map energyStateAttributes) { + logger.debug("Calling updateExtraInfoStateChannels for device: {}", getDeviceId()); + String instantEnergyConsumption = (String) energyStateAttributes.get(CAP_EXTRA_ATTR_INSTANT_POWER); + String filterUsed = (String) energyStateAttributes.get(CAP_EXTRA_ATTR_FILTER_USED_TIME); + String filterLifetime = (String) energyStateAttributes.get(CAP_EXTRA_ATTR_FILTER_MAX_TIME_TO_USE); + if (instantEnergyConsumption == null) { + updateState(currentEnergyConsumptionChannelUID, UnDefType.NULL); + } else { + try { + double ip = Double.parseDouble(instantEnergyConsumption); + updateState(currentEnergyConsumptionChannelUID, new QuantityType<>(ip, Units.WATT_HOUR)); + } catch (NumberFormatException e) { + updateState(currentEnergyConsumptionChannelUID, UnDefType.UNDEF); + } + } + + if (filterLifetime == null || filterUsed == null) { + updateState(remainingFilterChannelUID, UnDefType.NULL); + } else { + try { + double used = Double.parseDouble(filterUsed); + double max = Double.parseDouble(filterLifetime); + double perc = (1 - (used / max)) * 100; + updateState(remainingFilterChannelUID, new QuantityType<>(perc, Units.PERCENT)); + } catch (NumberFormatException ex) { + updateState(remainingFilterChannelUID, UnDefType.UNDEF); + } + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQBridge.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQBridge.java new file mode 100644 index 0000000000000..81f928803f5ac --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQBridge.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.internal.discovery.LGThinqDiscoveryService; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.SnapshotDefinition; + +/** + * The {@link LGThinQBridge} - Specific methods for discovery integration + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface LGThinQBridge { + /** + * Register Discovery Listener + * + * @param listener + */ + void registerDiscoveryListener(LGThinqDiscoveryService listener); + + /** + * Registry a device Thing to the bridge + * + * @param thing Thing to be registered. + */ + void registryListenerThing( + LGThinQAbstractDeviceHandler thing); + + /** + * Unregistry the thing + * + * @param thing to be unregistered + */ + void unRegistryListenerThing( + LGThinQAbstractDeviceHandler thing); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQBridgeHandler.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQBridgeHandler.java new file mode 100644 index 0000000000000..6cc72119b73b2 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQBridgeHandler.java @@ -0,0 +1,374 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THINQ_CONNECTION_DATA_FILE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THINQ_USER_DATA_FOLDER; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.internal.LGThinQBridgeConfiguration; +import org.openhab.binding.lgthinq.internal.discovery.LGThinqDiscoveryService; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory.LGThinQGeneralApiClientService; +import org.openhab.binding.lgthinq.lgservices.api.TokenManager; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.errors.RefreshTokenException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGDevice; +import org.openhab.binding.lgthinq.lgservices.model.SnapshotDefinition; +import org.openhab.core.config.core.status.ConfigStatusMessage; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.ConfigStatusBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinQBridgeHandler} - connect to the LG Account and get information about the user and registered + * devices of that user. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQBridgeHandler extends ConfigStatusBridgeHandler implements LGThinQBridge { + + private final Map> lGDeviceRegister = new ConcurrentHashMap<>(); + private final Map lastDevicesDiscovered = new ConcurrentHashMap<>(); + + private static final LGThinqDiscoveryService DUMMY_DISCOVERY_SERVICE = new LGThinqDiscoveryService(); + static { + var logger = LoggerFactory.getLogger(LGThinQBridgeHandler.class); + try { + File directory = new File(THINQ_USER_DATA_FOLDER); + if (!directory.exists()) { + if (!directory.mkdir()) { + throw new LGThinqException("Can't create directory for userdata thinq"); + } + } + } catch (Exception e) { + logger.warn("Unable to setup thinq userdata directory: {}", e.getMessage()); + } + } + private final Logger logger = LoggerFactory.getLogger(LGThinQBridgeHandler.class); + private LGThinQBridgeConfiguration lgthinqConfig = new LGThinQBridgeConfiguration(); + private final TokenManager tokenManager; + private LGThinqDiscoveryService discoveryService = DUMMY_DISCOVERY_SERVICE; + private final @Nullable LGThinQGeneralApiClientService lgApiClient; + private @Nullable ScheduledFuture devicePollingJob; + private final HttpClientFactory httpClientFactory; + + public LGThinQBridgeHandler(Bridge bridge, HttpClientFactory httpClientFactory) { + super(bridge); + this.httpClientFactory = httpClientFactory; + tokenManager = new TokenManager(httpClientFactory.getCommonHttpClient()); + lgApiClient = LGThinQApiClientServiceFactory.newGeneralApiClientService(httpClientFactory); + lgDevicePollingRunnable = new LGDevicePollingRunnable(bridge.getUID().getId()); + } + + public HttpClientFactory getHttpClientFactory() { + return httpClientFactory; + } + + final ReentrantLock pollingLock = new ReentrantLock(); + + /** + * Abstract Runnable Polling Class to schedule synchronization status of the Bridge Thing Kinds ! + */ + abstract class PollingRunnable implements Runnable { + protected final String bridgeName; + protected LGThinQBridgeConfiguration lgthinqConfig = new LGThinQBridgeConfiguration(); + + PollingRunnable(String bridgeName) { + this.bridgeName = bridgeName; + } + + @Override + public void run() { + try { + pollingLock.lock(); + // check if configuration file already exists + if (tokenManager.isOauthTokenRegistered(bridgeName)) { + logger.debug( + "Token authentication process has been already done. Skip first authentication process."); + try { + tokenManager.getValidRegisteredToken(bridgeName); + } catch (IOException e) { + logger.error("Unexpected error reading LGThinq TokenFile", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "@text/error.toke-file-corrupted"); + return; + } catch (RefreshTokenException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "@text/error.toke-refresh"); + logger.error("Error refreshing token", e); + return; + } + } else { + try { + tokenManager.oauthFirstRegistration(bridgeName, lgthinqConfig.getLanguage(), + lgthinqConfig.getCountry(), lgthinqConfig.getUsername(), lgthinqConfig.getPassword(), + lgthinqConfig.getAlternativeServer()); + tokenManager.getValidRegisteredToken(bridgeName); + logger.debug("Successful getting token from LG API"); + } catch (IOException e) { + logger.debug( + "I/O error accessing json token configuration file. Updating Bridge Status to OFFLINE.", + e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/error.toke-file-access-error"); + return; + } catch (LGThinqException e) { + logger.debug("Error accessing LG API. Updating Bridge Status to OFFLINE.", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/error.lgapi-communication-error"); + return; + } + } + if (thing.getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + + try { + doConnectedRun(); + } catch (Exception e) { + logger.error("Unexpected error getting device list from LG account", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/error.lgapi-getting-devices"); + } + + } finally { + pollingLock.unlock(); + } + } + + protected abstract void doConnectedRun() throws LGThinqException; + } + + @Override + public void registerDiscoveryListener(LGThinqDiscoveryService listener) { + discoveryService = listener; + } + + @Override + public void registryListenerThing( + LGThinQAbstractDeviceHandler thing) { + if (lGDeviceRegister.get(thing.getDeviceId()) == null) { + lGDeviceRegister.put(thing.getDeviceId(), thing); + // remove device from discovery list, if exists. + LGDevice device = lastDevicesDiscovered.get(thing.getDeviceId()); + if (device != null && discoveryService != DUMMY_DISCOVERY_SERVICE) { + discoveryService.removeLgDeviceDiscovery(device); + } + } + } + + @Override + public void unRegistryListenerThing( + LGThinQAbstractDeviceHandler thing) { + lGDeviceRegister.remove(thing.getDeviceId()); + } + + private final LGDevicePollingRunnable lgDevicePollingRunnable; + + private LGThinQGeneralApiClientService getLgApiClient() { + return Objects.requireNonNull(lgApiClient, "Not expected lgApiClient null. It most likely a bug"); + } + + class LGDevicePollingRunnable extends PollingRunnable { + public LGDevicePollingRunnable(String bridgeName) { + super(bridgeName); + } + + @Override + protected void doConnectedRun() throws LGThinqException { + Map lastDevicesDiscoveredCopy = new HashMap<>(lastDevicesDiscovered); + List devices = getLgApiClient().listAccountDevices(bridgeName); + // if not registered yet, and not discovered before, then add to discovery list. + devices.forEach(device -> { + String deviceId = device.getDeviceId(); + logger.debug("Device found: {}", deviceId); + if (lGDeviceRegister.get(deviceId) == null && !lastDevicesDiscovered.containsKey(deviceId)) { + logger.debug("Adding new LG Device to things registry with id:{}", deviceId); + if (discoveryService != DUMMY_DISCOVERY_SERVICE) { + discoveryService.addLgDeviceDiscovery(device); + } + } else { + if (discoveryService != DUMMY_DISCOVERY_SERVICE && lGDeviceRegister.get(deviceId) != null) { + discoveryService.removeLgDeviceDiscovery(device); + } + } + lastDevicesDiscovered.put(deviceId, device); + lastDevicesDiscoveredCopy.remove(deviceId); + }); + // the rest in lastDevicesDiscoveredCopy is not more registered in LG API. Remove from discovery + lastDevicesDiscoveredCopy.forEach((deviceId, device) -> { + logger.debug("LG Device '{}' removed.", deviceId); + lastDevicesDiscovered.remove(deviceId); + + LGThinQAbstractDeviceHandler deviceThing = lGDeviceRegister + .get(deviceId); + if (deviceThing != null) { + deviceThing.onDeviceRemoved(); + } + if (discoveryService != DUMMY_DISCOVERY_SERVICE && deviceThing != null) { + discoveryService.removeLgDeviceDiscovery(device); + } + }); + + lGDeviceRegister.values().forEach(LGThinQAbstractDeviceHandler::refreshStatus); + } + } + + @Override + public Collection getConfigStatus() { + List resultList = new ArrayList<>(); + if (lgthinqConfig.username.isEmpty()) { + resultList.add(ConfigStatusMessage.Builder.error("USERNAME").withMessageKeySuffix("missing field") + .withArguments("username").build()); + } + if (lgthinqConfig.password.isEmpty()) { + resultList.add(ConfigStatusMessage.Builder.error("PASSWORD").withMessageKeySuffix("missing field") + .withArguments("password").build()); + } + if (lgthinqConfig.language.isEmpty()) { + resultList.add(ConfigStatusMessage.Builder.error("LANGUAGE").withMessageKeySuffix("missing field") + .withArguments("language").build()); + } + if (lgthinqConfig.country.isEmpty()) { + resultList.add(ConfigStatusMessage.Builder.error("COUNTRY").withMessageKeySuffix("missing field") + .withArguments("country").build()); + + } + return resultList; + } + + @Override + @SuppressWarnings("null") + public void handleRemoval() { + if (devicePollingJob != null) { + devicePollingJob.cancel(true); + } + tokenManager.cleanupTokenRegistry( + Objects.requireNonNull(getBridge(), "Not expected bridge null here").getUID().getId()); + super.handleRemoval(); + } + + @Override + @SuppressWarnings("null") + public void dispose() { + if (devicePollingJob != null) { + devicePollingJob.cancel(true); + devicePollingJob = null; + } + } + + @Override + public T getConfigAs(Class configurationClass) { + return super.getConfigAs(configurationClass); + } + + @Override + public void initialize() { + logger.debug("Initializing LGThinq bridge handler."); + lgthinqConfig = getConfigAs(LGThinQBridgeConfiguration.class); + lgDevicePollingRunnable.lgthinqConfig = lgthinqConfig; + if (lgthinqConfig.username.isEmpty() || lgthinqConfig.password.isEmpty() || lgthinqConfig.language.isEmpty() + || lgthinqConfig.country.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/error.mandotory-fields-missing"); + } else { + // updateStatus(ThingStatus.UNKNOWN); + startLGThinqDevicePolling(); + } + } + + @Override + public void handleConfigurationUpdate(Map configurationParameters) { + logger.debug("Bridge Configuration was updated. Cleaning the token registry file"); + File f = new File(String.format(THINQ_CONNECTION_DATA_FILE, getThing().getUID().getId())); + if (f.isFile()) { + // file exists. Delete it + if (!f.delete()) { + logger.error("Unexpected error deleting file:{}", f.getAbsolutePath()); + } + } + super.handleConfigurationUpdate(configurationParameters); + } + + @SuppressWarnings("null") + private void startLGThinqDevicePolling() { + // stop current scheduler, if any + if (devicePollingJob != null && !devicePollingJob.isDone()) { + devicePollingJob.cancel(true); + } + long poolingInterval; + int configPollingInterval = lgthinqConfig.getPollingIntervalSec(); + // It's not recommended to polling for resources in LG API short intervals to do not enter in BlackList + if (configPollingInterval < 300 && configPollingInterval != 0) { + poolingInterval = TimeUnit.SECONDS.toSeconds(300); + logger.info("Wrong configuration value for pooling interval. Using default value: {}s", poolingInterval); + } else { + if (configPollingInterval == 0) { + logger.info("LG's discovery pooling disabled (configured as zero)"); + return; + } + poolingInterval = configPollingInterval; + } + // submit instantlly and schedule for the next polling interval. + runDiscovery(); + devicePollingJob = scheduler.scheduleWithFixedDelay(lgDevicePollingRunnable, 2, poolingInterval, + TimeUnit.SECONDS); + } + + public void runDiscovery() { + scheduler.submit(lgDevicePollingRunnable); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + public void unregisterDiscoveryListener() { + discoveryService = DUMMY_DISCOVERY_SERVICE; + } + + /** + * Registry the OSGi services used by this Bridge. + * Eventually, the Discovery Service will be activated with this bridge as argument. + * + * @return Services to be registered to OSGi. + */ + public Collection> getServices() { + return Set.of(LGThinqDiscoveryService.class); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQDishWasherHandler.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQDishWasherHandler.java new file mode 100644 index 0000000000000..05eae198b51f7 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQDishWasherHandler.java @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_POWER_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_DASHBOARD_GRP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_COURSE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_DOOR_LOCK_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_PROCESS_STATE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMAIN_TIME_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_SMART_COURSE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_STATE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_DEVICE_ALIAS; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_MODEL_URL_INFO; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_MACHINE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_TOWER; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_DW_DOOR_STATE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_DW_PROCESS_STATE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_DW_STATE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WMD_POWER_OFF_VALUE; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.internal.LGThinQStateDescriptionProvider; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientService; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory; +import org.openhab.binding.lgthinq.lgservices.LGThinQDishWasherApiClientService; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherSnapshot; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinQDishWasherHandler} Handle the Dish Washer Things + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQDishWasherHandler extends LGThinQAbstractDeviceHandler { + + private final LGThinQStateDescriptionProvider stateDescriptionProvider; + private final ChannelUID courseChannelUID; + private final ChannelUID remainTimeChannelUID; + private final ChannelUID stateChannelUID; + private final ChannelUID processStateChannelUID; + private final ChannelUID doorLockChannelUID; + + public final ChannelGroupUID channelGroupDashboardUID; + + private final Logger logger = LoggerFactory.getLogger(LGThinQDishWasherHandler.class); + + private final LGThinQDishWasherApiClientService lgThinqDishWasherApiClientService; + + public LGThinQDishWasherHandler(Thing thing, LGThinQStateDescriptionProvider stateDescriptionProvider, + ItemChannelLinkRegistry itemChannelLinkRegistry, HttpClientFactory httpClientFactory) { + super(thing, stateDescriptionProvider, itemChannelLinkRegistry); + this.stateDescriptionProvider = stateDescriptionProvider; + lgThinqDishWasherApiClientService = LGThinQApiClientServiceFactory.newDishWasherApiClientService(lgPlatformType, + httpClientFactory); + channelGroupDashboardUID = new ChannelGroupUID(getThing().getUID(), CHANNEL_DASHBOARD_GRP_ID); + courseChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_COURSE_ID); + stateChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_STATE_ID); + processStateChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_PROCESS_STATE_ID); + remainTimeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_REMAIN_TIME_ID); + doorLockChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_DOOR_LOCK_ID); + } + + private void loadOptionsCourse(DishWasherCapability cap, ChannelUID courseChannel) { + List optionsCourses = new ArrayList<>(); + cap.getCourses().forEach((k, v) -> optionsCourses.add(new StateOption(k, emptyIfNull(v.getCourseName())))); + stateDescriptionProvider.setStateOptions(courseChannel, optionsCourses); + } + + @Override + public void updateChannelDynStateDescription() throws LGThinqApiException { + DishWasherCapability dwCap = getCapabilities(); + + List options = new ArrayList<>(); + dwCap.getStateFeat().getValuesMapping() + .forEach((k, v) -> options.add(new StateOption(k, keyIfValueNotFound(CAP_DW_STATE, v)))); + stateDescriptionProvider.setStateOptions(stateChannelUID, options); + + loadOptionsCourse(dwCap, courseChannelUID); + + List optionsDoor = new ArrayList<>(); + dwCap.getDoorStateFeat().getValuesMapping() + .forEach((k, v) -> optionsDoor.add(new StateOption(k, keyIfValueNotFound(CAP_DW_DOOR_STATE, v)))); + stateDescriptionProvider.setStateOptions(doorLockChannelUID, optionsDoor); + + List optionsPre = new ArrayList<>(); + dwCap.getProcessState().getValuesMapping() + .forEach((k, v) -> optionsPre.add(new StateOption(k, keyIfValueNotFound(CAP_DW_PROCESS_STATE, v)))); + stateDescriptionProvider.setStateOptions(processStateChannelUID, optionsPre); + } + + @Override + public LGThinQApiClientService getLgThinQAPIClientService() { + return lgThinqDishWasherApiClientService; + } + + @Override + protected Logger getLogger() { + return logger; + } + + @Override + protected void updateDeviceChannels(DishWasherSnapshot shot) { + updateState("dashboard#" + CHANNEL_AC_POWER_ID, + (DevicePowerState.DV_POWER_ON.equals(shot.getPowerStatus()) ? OnOffType.ON : OnOffType.OFF)); + updateState(stateChannelUID, new StringType(shot.getState())); + updateState(processStateChannelUID, new StringType(shot.getProcessState())); + updateState(courseChannelUID, new StringType(shot.getCourse())); + updateState(doorLockChannelUID, new StringType(shot.getDoorLock())); + updateState(remainTimeChannelUID, new StringType(shot.getRemainingTime())); + } + + @Override + protected DeviceTypes getDeviceType() { + if (THING_TYPE_WASHING_MACHINE.equals(getThing().getThingTypeUID())) { + return DeviceTypes.WASHERDRYER_MACHINE; + } else if (THING_TYPE_WASHING_TOWER.equals(getThing().getThingTypeUID())) { + return DeviceTypes.WASHER_TOWER; + } else { + throw new IllegalArgumentException( + "DeviceTypeUuid [" + getThing().getThingTypeUID() + "] not expected for WashingTower/Machine"); + } + } + + @Override + protected void processCommand(AsyncCommandParams params) { + logger.warn("Command {} to the channel {} not supported. Ignored.", params.command, params.channelUID); + } + + @Override + public String getDeviceAlias() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_DEVICE_ALIAS)); + } + + @Override + public String getDeviceUriJsonConfig() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_MODEL_URL_INFO)); + } + + @Override + public void onDeviceRemoved() { + } + + /** + * Put the channels in default state if the device is disconnected or gone. + */ + @Override + public void onDeviceDisconnected() { + updateState(CHANNEL_AC_POWER_ID, OnOffType.OFF); + updateState(CHANNEL_WMD_STATE_ID, new StringType(WMD_POWER_OFF_VALUE)); + updateState(CHANNEL_WMD_COURSE_ID, new StringType("NOT_SELECTED")); + updateState(CHANNEL_WMD_SMART_COURSE_ID, new StringType("NOT_SELECTED")); + updateState(CHANNEL_WMD_DOOR_LOCK_ID, new StringType("DOOR_LOCK_OFF")); + updateState(CHANNEL_WMD_REMAIN_TIME_ID, new StringType("00:00")); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQFridgeHandler.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQFridgeHandler.java new file mode 100644 index 0000000000000..f043771cf9802 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQFridgeHandler.java @@ -0,0 +1,413 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_DASHBOARD_GRP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_EXTENDED_INFO_GRP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_ACTIVE_SAVING; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_DOOR_OPEN; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_EXPRESS_COOL_MODE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_EXPRESS_FREEZE_MODE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_FREEZER_TEMP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_FRESH_AIR_FILTER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_FRIDGE_TEMP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_ICE_PLUS; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_REF_TEMP_UNIT; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_SMART_SAVING_MODE_V2; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_SMART_SAVING_SWITCH_V1; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_VACATION_MODE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_FR_WATER_FILTER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_DEVICE_ALIAS; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_MODEL_URL_INFO; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_FRESH_AIR_FILTER_MAP; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_WATER_FILTER; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_PLATFORM_TYPE_V2; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_CELSIUS_UNIT_VALUES; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_DOOR_CLOSE_VALUES; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_DOOR_OPEN_VALUES; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_FAHRENHEIT_UNIT_VALUES; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_CELSIUS; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_FAHRENHEIT; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.internal.LGThinQStateDescriptionProvider; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientService; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory; +import org.openhab.binding.lgthinq.lgservices.LGThinQFridgeApiClientService; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCapability; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.StateOption; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinQFridgeHandler} Handle Fridge things + * + * @author Nemer Daud - Initial contribution + * @author Arne Seime - Complementary sensors + */ +@NonNullByDefault +public class LGThinQFridgeHandler extends LGThinQAbstractDeviceHandler { + public final ChannelGroupUID channelGroupExtendedInfoUID; + public final ChannelGroupUID channelGroupDashboardUID; + private final ChannelUID fridgeTempChannelUID; + private final ChannelUID freezerTempChannelUID; + private final ChannelUID doorChannelUID; + private final ChannelUID smartSavingModeChannelUID; + private final ChannelUID activeSavingChannelUID; + private final ChannelUID icePlusChannelUID; + private final ChannelUID expressFreezeModeChannelUID; + private final ChannelUID expressCoolModeChannelUID; + private final ChannelUID vacationModeChannelUID; + private final ChannelUID freshAirFilterChannelUID; + private final ChannelUID waterFilterChannelUID; + private final ChannelUID tempUnitUID; + private String tempUnit = RE_TEMP_UNIT_CELSIUS; + private final Logger logger = LoggerFactory.getLogger(LGThinQFridgeHandler.class); + + private final LGThinQFridgeApiClientService lgThinqFridgeApiClientService; + + public LGThinQFridgeHandler(Thing thing, LGThinQStateDescriptionProvider stateDescriptionProvider, + ItemChannelLinkRegistry itemChannelLinkRegistry, HttpClientFactory httpClientFactory) { + super(thing, stateDescriptionProvider, itemChannelLinkRegistry); + lgThinqFridgeApiClientService = LGThinQApiClientServiceFactory.newFridgeApiClientService(lgPlatformType, + httpClientFactory); + channelGroupDashboardUID = new ChannelGroupUID(getThing().getUID(), CHANNEL_DASHBOARD_GRP_ID); + channelGroupExtendedInfoUID = new ChannelGroupUID(getThing().getUID(), CHANNEL_EXTENDED_INFO_GRP_ID); + fridgeTempChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_FRIDGE_TEMP_ID); + freezerTempChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_FREEZER_TEMP_ID); + doorChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_DOOR_OPEN); + tempUnitUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_REF_TEMP_UNIT); + icePlusChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_ICE_PLUS); + expressFreezeModeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_EXPRESS_FREEZE_MODE); + expressCoolModeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_EXPRESS_COOL_MODE); + vacationModeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_VACATION_MODE); + smartSavingModeChannelUID = new ChannelUID(channelGroupDashboardUID, + LG_API_PLATFORM_TYPE_V2.equals(lgPlatformType) ? CHANNEL_FR_SMART_SAVING_MODE_V2 + : CHANNEL_FR_SMART_SAVING_SWITCH_V1); + activeSavingChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_FR_ACTIVE_SAVING); + freshAirFilterChannelUID = new ChannelUID(channelGroupExtendedInfoUID, CHANNEL_FR_FRESH_AIR_FILTER); + waterFilterChannelUID = new ChannelUID(channelGroupExtendedInfoUID, CHANNEL_FR_WATER_FILTER); + } + + private Unit getTemperatureUnit(FridgeCanonicalSnapshot shot) { + if (!(RE_CELSIUS_UNIT_VALUES.contains(shot.getTempUnit()) + || RE_FAHRENHEIT_UNIT_VALUES.contains(shot.getTempUnit()))) { + logger.warn( + "Temperature Unit not recognized (must be Celsius or Fahrenheit). Ignoring and considering Celsius as default"); + return SIUnits.CELSIUS; + } + return RE_CELSIUS_UNIT_VALUES.contains(shot.getTempUnit()) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT; + } + + @Override + protected void updateDeviceChannels(FridgeCanonicalSnapshot shot) { + Unit unTemp = getTemperatureUnit(shot); + if (isLinked(fridgeTempChannelUID)) { + updateState(fridgeTempChannelUID, + new QuantityType<>(decodeTempValue(fridgeTempChannelUID, shot.getFridgeTemp().intValue()), unTemp)); + } + if (isLinked(freezerTempChannelUID)) { + updateState(freezerTempChannelUID, new QuantityType<>( + decodeTempValue(freezerTempChannelUID, shot.getFreezerTemp().intValue()), unTemp)); + } + if (isLinked(doorChannelUID)) { + updateState(doorChannelUID, parseDoorStatus(shot.getDoorStatus())); + } + if (isLinked(expressFreezeModeChannelUID)) { + updateState(expressFreezeModeChannelUID, new StringType(shot.getExpressMode())); + } + if (isLinked(expressCoolModeChannelUID)) { + updateState(expressCoolModeChannelUID, + "ON".equals(shot.getExpressCoolMode()) ? OnOffType.ON : OnOffType.OFF); + } + if (isLinked(vacationModeChannelUID)) { + updateState(vacationModeChannelUID, "ON".equals(shot.getEcoFriendlyMode()) ? OnOffType.ON : OnOffType.OFF); + } + if (isLinked(freshAirFilterChannelUID)) { + updateState(freshAirFilterChannelUID, new StringType(shot.getFreshAirFilterState())); + } + if (isLinked(waterFilterChannelUID)) { + updateState(waterFilterChannelUID, new StringType(shot.getWaterFilterUsedMonth())); + } + + updateState(tempUnitUID, new StringType(shot.getTempUnit())); + if (!tempUnit.equals(shot.getTempUnit())) { + tempUnit = shot.getTempUnit(); + try { + // force update states after first snapshot fetched to fit changes in temperature unit + updateChannelDynStateDescription(); + } catch (Exception ex) { + logger.error("Unexpected error updating dynamic state description.", ex); + } + } + } + + private State parseDoorStatus(String doorStatus) { + if (RE_DOOR_CLOSE_VALUES.contains(doorStatus)) { + return OpenClosedType.CLOSED; + } else if (RE_DOOR_OPEN_VALUES.contains(doorStatus)) { + return OpenClosedType.OPEN; + } else { + return UnDefType.UNDEF; + } + } + + protected Integer decodeTempValue(ChannelUID ch, Integer value) { + FridgeCapability refCap; + try { + refCap = getCapabilities(); + } catch (LGThinqApiException e) { + logger.error("Error getting capability of the device. It's mostly like a bug", e); + return 0; + } + // temperature channels are little different. First we need to get the tempUnit in the first snapshot, + Map convertionMap = getConvertionMap(ch, refCap); + String strValue = convertionMap.get(value.toString()); + if (strValue == null) { + logger.error( + "Temperature value informed [{}] can't be converted based on the cap file. It mostly like a bug", + value); + return 0; + } + try { + return Integer.valueOf(strValue); + } catch (Exception ex) { + logger.error("Temperature value informed [{}] can't be parsed to number. It mostly like a bug", value, ex); + return 0; + } + } + + protected Integer encodeTempValue(ChannelUID ch, Integer value) { + FridgeCapability refCap; + try { + refCap = getCapabilities(); + } catch (LGThinqApiException e) { + logger.error("Error getting capability of the device. It's mostly like a bug", e); + return 0; + } + // temperature channels are little different. First we need to get the tempUnit in the first snapshot, + final Map convertionMap = getConvertionMap(ch, refCap); + final Map invertedMap = new HashMap<>(); + convertionMap.forEach((k, v) -> { + invertedMap.put(v, k); + }); + + String strValue = invertedMap.get(value.toString()); + if (strValue == null) { + logger.error("Temperature value informed can't be converted based on the cap file. It mostly like a bug"); + return 0; + } + try { + return Integer.valueOf(strValue); + } catch (Exception ex) { + logger.error("Temperature value converted can't be cast to Integer. It mostly like a bug", ex); + return 0; + } + } + + private Map getConvertionMap(ChannelUID ch, FridgeCapability refCap) { + Map convertionMap; + if (fridgeTempChannelUID.equals(ch)) { + convertionMap = RE_TEMP_UNIT_FAHRENHEIT.equals(tempUnit) ? refCap.getFridgeTempFMap() + : refCap.getFridgeTempCMap(); + } else if (freezerTempChannelUID.equals(ch)) { + convertionMap = RE_TEMP_UNIT_FAHRENHEIT.equals(tempUnit) ? refCap.getFreezerTempFMap() + : refCap.getFreezerTempCMap(); + } else { + throw new IllegalStateException("Conversion Map Channel temperature not mapped. It's most likely a bug"); + } + return convertionMap; + } + + @Override + public LGThinQApiClientService getLgThinQAPIClientService() { + return lgThinqFridgeApiClientService; + } + + @Override + protected Logger getLogger() { + return logger; + } + + protected DeviceTypes getDeviceType() { + return DeviceTypes.AIR_CONDITIONER; + } + + @Override + public String getDeviceAlias() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_DEVICE_ALIAS)); + } + + @Override + public String getDeviceUriJsonConfig() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_MODEL_URL_INFO)); + } + + @Override + public void onDeviceRemoved() { + } + + @Override + public void onDeviceDisconnected() { + } + + @Override + public void updateChannelDynStateDescription() throws LGThinqApiException { + FridgeCapability cap = getCapabilities(); + manageDynChannel(icePlusChannelUID, CHANNEL_FR_ICE_PLUS, "Switch", !cap.getIcePlusMap().isEmpty()); + manageDynChannel(expressFreezeModeChannelUID, CHANNEL_FR_EXPRESS_FREEZE_MODE, "String", + !cap.getExpressFreezeModeMap().isEmpty()); + manageDynChannel(expressCoolModeChannelUID, CHANNEL_FR_EXPRESS_COOL_MODE, "Switch", + cap.isExpressCoolModePresent()); + manageDynChannel(vacationModeChannelUID, CHANNEL_FR_VACATION_MODE, "Switch", cap.isEcoFriendlyModePresent()); + + Unit unTemp = getTemperatureUnit(getLastShot()); + if (SIUnits.CELSIUS.equals(unTemp)) { + loadChannelTempStateOption(cap.getFridgeTempCMap(), fridgeTempChannelUID, unTemp); + loadChannelTempStateOption(cap.getFreezerTempCMap(), freezerTempChannelUID, unTemp); + } else { + loadChannelTempStateOption(cap.getFridgeTempFMap(), fridgeTempChannelUID, unTemp); + loadChannelTempStateOption(cap.getFreezerTempFMap(), freezerTempChannelUID, unTemp); + } + loadChannelStateOption(cap.getActiveSavingMap(), activeSavingChannelUID); + loadChannelStateOption(cap.getExpressFreezeModeMap(), expressFreezeModeChannelUID); + loadChannelStateOption(cap.getActiveSavingMap(), activeSavingChannelUID); + loadChannelStateOption(cap.getSmartSavingMap(), smartSavingModeChannelUID); + loadChannelStateOption(cap.getTempUnitMap(), tempUnitUID); + loadChannelStateOption(CAP_RE_FRESH_AIR_FILTER_MAP, freshAirFilterChannelUID); + loadChannelStateOption(CAP_RE_WATER_FILTER, waterFilterChannelUID); + } + + private void loadChannelStateOption(Map cap, ChannelUID channelUID) { + final List faOptions = new ArrayList<>(); + cap.forEach((k, v) -> faOptions.add(new StateOption(k, v))); + stateDescriptionProvider.setStateOptions(channelUID, faOptions); + } + + private void loadChannelTempStateOption(Map cap, ChannelUID channelUID, Unit unTemp) { + final List faOptions = new ArrayList<>(); + cap.forEach((k, v) -> { + try { + QuantityType t = new QuantityType<>(Integer.valueOf(v), unTemp); + faOptions.add(new StateOption(t.toString(), t.toString())); + } catch (NumberFormatException ex) { + logger.debug("Error converting invalid temperature number: {}. This can be safely ignored", v); + } + }); + stateDescriptionProvider.setStateOptions(channelUID, faOptions); + } + + @Override + protected void processCommand(AsyncCommandParams params) throws LGThinqApiException { + FridgeCanonicalSnapshot lastShot = getLastShot(); + Map cmdSnap = lastShot.getRawData(); + Command command = params.command; + String simpleChannelUID; + simpleChannelUID = getSimpleChannelUID(params.channelUID); + switch (simpleChannelUID) { + case CHANNEL_FR_FREEZER_TEMP_ID: + case CHANNEL_FR_FRIDGE_TEMP_ID: { + int targetTemp; + if (command instanceof DecimalType) { + targetTemp = ((DecimalType) command).intValue(); + } else if (command instanceof QuantityType) { + targetTemp = ((QuantityType) command).intValue(); + } else { + logger.warn("Received command different of Numeric in TargetTemp Channel. Ignoring"); + break; + } + + if (CHANNEL_FR_FRIDGE_TEMP_ID.equals(simpleChannelUID)) { + targetTemp = encodeTempValue(fridgeTempChannelUID, targetTemp); + lgThinqFridgeApiClientService.setFridgeTemperature(getBridgeId(), getDeviceId(), getCapabilities(), + targetTemp, lastShot.getTempUnit(), cmdSnap); + } else { + targetTemp = encodeTempValue(freezerTempChannelUID, targetTemp); + lgThinqFridgeApiClientService.setFreezerTemperature(getBridgeId(), getDeviceId(), getCapabilities(), + targetTemp, lastShot.getTempUnit(), cmdSnap); + } + break; + } + case CHANNEL_FR_ICE_PLUS: { + if (command instanceof OnOffType) { + lgThinqFridgeApiClientService.setIcePlus(getBridgeId(), getDeviceId(), getCapabilities(), + OnOffType.ON.equals(command), cmdSnap); + } else { + logger.warn("Received command different of OnOff in IcePlus Channel. It's mostly like a bug"); + } + break; + } + case CHANNEL_FR_EXPRESS_FREEZE_MODE: { + String targetExpressMode; + if (command instanceof StringType) { + targetExpressMode = ((StringType) command).toString(); + } else { + logger.warn("Received command different of String in ExpressMode Channel. It's mostly like a bug"); + break; + } + + lgThinqFridgeApiClientService.setExpressMode(getBridgeId(), getDeviceId(), targetExpressMode); + break; + } + case CHANNEL_FR_EXPRESS_COOL_MODE: { + if (command instanceof OnOffType) { + lgThinqFridgeApiClientService.setExpressCoolMode(getBridgeId(), getDeviceId(), + OnOffType.ON.equals(command)); + } else { + logger.warn( + "Received command different of OnOffType in ExpressCoolMode Channel. It's mostly like a bug"); + } + break; + } + case CHANNEL_FR_VACATION_MODE: { + if (command instanceof OnOffType) { + lgThinqFridgeApiClientService.setEcoFriendlyMode(getBridgeId(), getDeviceId(), + OnOffType.ON.equals(command)); + } else { + logger.warn( + "Received command different of OnOffType in VacationMode Channel. It's most likely a bug"); + } + break; + } + default: { + logger.warn("Command {} to the channel {} not supported. Ignored.", command, params.channelUID); + } + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQWasherDryerHandler.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQWasherDryerHandler.java new file mode 100644 index 0000000000000..86d6ea45a2282 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/internal/handler/LGThinQWasherDryerHandler.java @@ -0,0 +1,449 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.internal.handler; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_AC_POWER_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_DASHBOARD_GRP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_CHILD_LOCK_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_COURSE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_DELAY_TIME_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_DOOR_LOCK_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_DRY_LEVEL_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_PROCESS_STATE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMAIN_TIME_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_COURSE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_GRP_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_RINSE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_SPIN; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_START_STOP; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_TEMP; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_RINSE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_SMART_COURSE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_SPIN_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_STAND_BY_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_STATE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_TEMP_LEVEL_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_DEVICE_ALIAS; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.PROP_INFO_MODEL_URL_INFO; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_DRYER; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_MACHINE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THING_TYPE_WASHING_TOWER; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_DR_DRY_LEVEL; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_WMD_PROCESS_STATE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_WMD_STATE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_WMD_TEMPERATURE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_WM_DICT_V2; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_WM_RINSE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_WM_SPIN; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WMD_COURSE_NOT_SELECTED_VALUE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WMD_POWER_OFF_VALUE; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.internal.LGThinQStateDescriptionProvider; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientService; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory; +import org.openhab.binding.lgthinq.lgservices.LGThinQWMApiClientService; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseDefinition; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseFunction; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseType; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerSnapshot; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.types.Command; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinQWasherDryerHandler} Handle Washer/Dryer And Washer Dryer Towers things + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQWasherDryerHandler + extends LGThinQAbstractDeviceHandler { + + private final LGThinQStateDescriptionProvider stateDescriptionProvider; + private final ChannelUID courseChannelUID; + private final ChannelUID remoteStartStopChannelUID; + private final ChannelUID remainTimeChannelUID; + private final ChannelUID delayTimeChannelUID; + private final ChannelUID spinChannelUID; + private final ChannelUID rinseChannelUID; + private final ChannelUID stateChannelUID; + private final ChannelUID processStateChannelUID; + private final ChannelUID childLockChannelUID; + private final ChannelUID dryLevelChannelUID; + private final ChannelUID temperatureChannelUID; + private final ChannelUID doorLockChannelUID; + private final ChannelUID standByModeChannelUID; + private final ChannelUID remoteStartFlagChannelUID; + private final ChannelUID remoteStartCourseChannelUID; + + public final ChannelGroupUID channelGroupRemoteStartUID; + public final ChannelGroupUID channelGroupDashboardUID; + + private final List remoteStartEnabledChannels = new CopyOnWriteArrayList<>(); + + private final Logger logger = LoggerFactory.getLogger(LGThinQWasherDryerHandler.class); + private final LGThinQWMApiClientService lgThinqWMApiClientService; + + public LGThinQWasherDryerHandler(Thing thing, LGThinQStateDescriptionProvider stateDescriptionProvider, + ItemChannelLinkRegistry itemChannelLinkRegistry, HttpClientFactory httpClientFactory) { + super(thing, stateDescriptionProvider, itemChannelLinkRegistry); + this.stateDescriptionProvider = stateDescriptionProvider; + lgThinqWMApiClientService = LGThinQApiClientServiceFactory.newWMApiClientService(lgPlatformType, + httpClientFactory); + channelGroupRemoteStartUID = new ChannelGroupUID(getThing().getUID(), CHANNEL_WMD_REMOTE_START_GRP_ID); + channelGroupDashboardUID = new ChannelGroupUID(getThing().getUID(), CHANNEL_DASHBOARD_GRP_ID); + courseChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_COURSE_ID); + dryLevelChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_DRY_LEVEL_ID); + stateChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_STATE_ID); + processStateChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_PROCESS_STATE_ID); + remainTimeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_REMAIN_TIME_ID); + delayTimeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_DELAY_TIME_ID); + temperatureChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_TEMP_LEVEL_ID); + doorLockChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_DOOR_LOCK_ID); + childLockChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_CHILD_LOCK_ID); + rinseChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_RINSE_ID); + spinChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_SPIN_ID); + standByModeChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_STAND_BY_ID); + remoteStartFlagChannelUID = new ChannelUID(channelGroupDashboardUID, CHANNEL_WMD_REMOTE_START_ID); + remoteStartStopChannelUID = new ChannelUID(channelGroupRemoteStartUID, CHANNEL_WMD_REMOTE_START_START_STOP); + remoteStartCourseChannelUID = new ChannelUID(channelGroupRemoteStartUID, CHANNEL_WMD_REMOTE_COURSE); + } + + @Override + protected void initializeThing(@Nullable ThingStatus bridgeStatus) { + super.initializeThing(bridgeStatus); + ThingBuilder builder = editThing() + .withoutChannels(this.getThing().getChannelsOfGroup(channelGroupRemoteStartUID.getId())); + updateThing(builder.build()); + remoteStartEnabledChannels.clear(); + } + + private void loadOptionsCourse(WasherDryerCapability cap, ChannelUID courseChannel) { + List optionsCourses = new ArrayList<>(); + cap.getCourses().forEach((k, v) -> optionsCourses.add(new StateOption(k, emptyIfNull(v.getCourseName())))); + stateDescriptionProvider.setStateOptions(courseChannel, optionsCourses); + } + + @Override + public void updateChannelDynStateDescription() throws LGThinqApiException { + WasherDryerCapability wmCap = getCapabilities(); + + List options = new ArrayList<>(); + wmCap.getStateFeat().getValuesMapping() + .forEach((k, v) -> options.add(new StateOption(k, keyIfValueNotFound(CAP_WMD_STATE, v)))); + stateDescriptionProvider.setStateOptions(stateChannelUID, options); + + loadOptionsCourse(wmCap, courseChannelUID); + + List optionsTemp = new ArrayList<>(); + wmCap.getTemperatureFeat().getValuesMapping() + .forEach((k, v) -> optionsTemp.add(new StateOption(k, keyIfValueNotFound(CAP_WMD_TEMPERATURE, v)))); + stateDescriptionProvider.setStateOptions(temperatureChannelUID, optionsTemp); + + List optionsDoor = new ArrayList<>(); + optionsDoor.add(new StateOption("0", "Unlocked")); + optionsDoor.add(new StateOption("1", "Locked")); + stateDescriptionProvider.setStateOptions(doorLockChannelUID, optionsDoor); + + List optionsSpin = new ArrayList<>(); + wmCap.getSpinFeat().getValuesMapping() + .forEach((k, v) -> optionsSpin.add(new StateOption(k, keyIfValueNotFound(CAP_WM_SPIN, v)))); + stateDescriptionProvider.setStateOptions(spinChannelUID, optionsSpin); + + List optionsRinse = new ArrayList<>(); + wmCap.getRinseFeat().getValuesMapping() + .forEach((k, v) -> optionsRinse.add(new StateOption(k, keyIfValueNotFound(CAP_WM_RINSE, v)))); + stateDescriptionProvider.setStateOptions(rinseChannelUID, optionsRinse); + + List optionsPre = new ArrayList<>(); + wmCap.getProcessState().getValuesMapping() + .forEach((k, v) -> optionsPre.add(new StateOption(k, keyIfValueNotFound(CAP_WMD_PROCESS_STATE, v)))); + stateDescriptionProvider.setStateOptions(processStateChannelUID, optionsPre); + + List optionsChildLock = new ArrayList<>(); + optionsChildLock.add(new StateOption("CHILDLOCK_OFF", "Unlocked")); + optionsChildLock.add(new StateOption("CHILDLOCK_ON", "Locked")); + stateDescriptionProvider.setStateOptions(childLockChannelUID, optionsChildLock); + + List optionsDryLevel = new ArrayList<>(); + wmCap.getDryLevel().getValuesMapping() + .forEach((k, v) -> optionsDryLevel.add(new StateOption(k, keyIfValueNotFound(CAP_DR_DRY_LEVEL, v)))); + stateDescriptionProvider.setStateOptions(dryLevelChannelUID, optionsDryLevel); + } + + @Override + public LGThinQApiClientService getLgThinQAPIClientService() { + return lgThinqWMApiClientService; + } + + @Override + protected Logger getLogger() { + return logger; + } + + @Override + protected void updateDeviceChannels(WasherDryerSnapshot shot) throws LGThinqApiException { + updateState("dashboard#" + CHANNEL_AC_POWER_ID, + (DevicePowerState.DV_POWER_ON.equals(shot.getPowerStatus()) ? OnOffType.ON : OnOffType.OFF)); + updateState(stateChannelUID, new StringType(shot.getState())); + updateState(processStateChannelUID, new StringType(shot.getProcessState())); + updateState(dryLevelChannelUID, new StringType(shot.getDryLevel())); + updateState(childLockChannelUID, new StringType(shot.getChildLock())); + updateState(courseChannelUID, new StringType(shot.getCourse())); + updateState(temperatureChannelUID, new StringType(shot.getTemperatureLevel())); + updateState(doorLockChannelUID, new StringType(shot.getDoorLock())); + updateState(remainTimeChannelUID, new StringType(shot.getRemainingTime())); + updateState(delayTimeChannelUID, new StringType(shot.getReserveTime())); + updateState(standByModeChannelUID, shot.isStandBy() ? OnOffType.ON : OnOffType.OFF); + updateState(remoteStartFlagChannelUID, shot.isRemoteStartEnabled() ? OnOffType.ON : OnOffType.OFF); + updateState(spinChannelUID, new StringType(shot.getSpin())); + updateState(rinseChannelUID, new StringType(shot.getRinse())); + Channel rsStartStopChannel = getThing().getChannel(remoteStartStopChannelUID); + final List dynChannels = new ArrayList<>(); + // only can have remote start channel is the WM is not in sleep mode, and remote start is enabled. + if (shot.isRemoteStartEnabled() && !shot.isStandBy()) { + ThingHandlerCallback callback = getCallback(); + if (rsStartStopChannel == null && callback != null) { + // === creating channel LaunchRemote + dynChannels.add( + createDynChannel(CHANNEL_WMD_REMOTE_START_START_STOP, remoteStartStopChannelUID, "Switch")); + dynChannels.add(createDynChannel(CHANNEL_WMD_REMOTE_COURSE, remoteStartCourseChannelUID, "String")); + // Just enabled remote start. Then is Off + updateState(remoteStartStopChannelUID, OnOffType.OFF); + // === creating selectable channels for the Course (if any) + WasherDryerCapability cap = getCapabilities(); + loadOptionsCourse(cap, remoteStartCourseChannelUID); + updateState(remoteStartCourseChannelUID, new StringType(cap.getDefaultCourseId())); + + CourseDefinition courseDef = cap.getCourses().get(cap.getDefaultCourseId()); + if (WMD_COURSE_NOT_SELECTED_VALUE.equals(shot.getSmartCourse()) && courseDef != null) { + // only create selectable channels if the course is not a smart course. Smart courses have + // already predefined + // the functions values + for (CourseFunction f : courseDef.getFunctions()) { + if (!f.isSelectable()) { + // only for selectable features + continue; + } + // handle well know dynamic fields + FeatureDefinition fd = cap.getFeatureDefinition(f.getValue()); + ChannelUID targetChannel; + ChannelUID refChannel; + if (!FeatureDefinition.NULL_DEFINITION.equals(fd)) { + targetChannel = new ChannelUID(channelGroupRemoteStartUID, fd.getChannelId()); + refChannel = new ChannelUID(channelGroupDashboardUID, fd.getRefChannelId()); + dynChannels.add(createDynChannel(fd.getChannelId(), targetChannel, + translateFeatureToItemType(fd.getDataType()))); + if (CAP_WM_DICT_V2.containsKey(f.getValue())) { + // if the function has translation dictionary (I hope so), then the values in + // the selectable channel will be translated to something more readable + List options = new ArrayList<>(); + for (String v : f.getSelectableValues()) { + Map values = CAP_WM_DICT_V2.get(f.getValue()); + if (values != null) { + // Canonical Value is the KEY (@...) that represents a constant in the + // definition + // that can be translated to a human description + String canonicalValue = Objects.requireNonNullElse(fd.getValuesMapping().get(v), + v); + options.add(new StateOption(v, keyIfValueNotFound(values, canonicalValue))); + stateDescriptionProvider.setStateOptions(targetChannel, options); + } + } + } + // update state with the default referenced channel + updateState(targetChannel, new StringType(getItemLinkedValue(refChannel))); + } + } + } + + remoteStartEnabledChannels.addAll(dynChannels); + + } + } else if (!remoteStartEnabledChannels.isEmpty()) { + ThingBuilder builder = editThing().withoutChannels(remoteStartEnabledChannels); + updateThing(builder.build()); + remoteStartEnabledChannels.clear(); + } + } + + @Override + protected DeviceTypes getDeviceType() { + if (THING_TYPE_WASHING_MACHINE.equals(getThing().getThingTypeUID())) { + return DeviceTypes.WASHERDRYER_MACHINE; + } else if (THING_TYPE_WASHING_TOWER.equals(getThing().getThingTypeUID())) { + return DeviceTypes.WASHER_TOWER; + } else if (THING_TYPE_DRYER.equals(getThing().getThingTypeUID())) { + return DeviceTypes.WASHER_TOWER; + } else { + throw new IllegalArgumentException( + "DeviceTypeUuid [" + getThing().getThingTypeUID() + "] not expected for WashingTower/Machine"); + } + } + + private Map getRemoteStartData() throws LGThinqApiException { + WasherDryerSnapshot lastShot = getLastShot(); + if (lastShot.getRawData().isEmpty()) { + return lastShot.getRawData(); + } + String selectedCourse = getItemLinkedValue(remoteStartCourseChannelUID); + if (selectedCourse == null) { + logger.warn("Remote Start Channel must be linked to proceed with remote start."); + return Collections.emptyMap(); + } + WasherDryerCapability cap = getCapabilities(); + Map rawData = lastShot.getRawData(); + Map data = new HashMap<>(); + CommandDefinition cmd = cap.getCommandsDefinition().get(cap.getCommandRemoteStart()); + if (cmd == null) { + logger.error("Command for Remote Start not found in the Washer descriptor. It's most likely a bug"); + return Collections.emptyMap(); + } + Map cmdData = cmd.getData(); + // 1st - copy snapshot data to command + cmdData.forEach((k, v) -> { + data.put(k, rawData.getOrDefault(k, v)); + }); + // 2nd - replace remote start data with selected course values + CourseDefinition selCourseDef = cap.getCourses().get(selectedCourse); + if (selCourseDef != null) { + selCourseDef.getFunctions().forEach(f -> { + data.put(f.getValue(), f.getDefaultValue()); + }); + } + String smartCourse = lastShot.getSmartCourse(); + data.put(cap.getDefaultCourseFieldName(), selectedCourse); + data.put(cap.getDefaultSmartCourseFeatName(), smartCourse); + CourseType courseType = Objects + .requireNonNull(cap.getCourses().get("NOT_SELECTED".equals(smartCourse) ? selectedCourse : smartCourse), + "NOT_SELECTED should be hardcoded. It most likely a bug") + .getCourseType(); + data.put("courseType", courseType.getValue()); + // 3rd - replace custom selectable features with channel's ones. + for (Channel c : remoteStartEnabledChannels) { + String value = Objects.requireNonNullElse(getItemLinkedValue(c.getUID()), ""); + String simpleChannelUID = getSimpleChannelUID(c.getUID().getId()); + switch (simpleChannelUID) { + case CHANNEL_WMD_REMOTE_START_RINSE: + data.put(cap.getRinseFeat().getName(), value); + break; + case CHANNEL_WMD_REMOTE_START_TEMP: + data.put(cap.getTemperatureFeat().getName(), value); + break; + case CHANNEL_WMD_REMOTE_START_SPIN: + data.put(cap.getSpinFeat().getName(), value); + break; + default: + logger.warn("channel [{}] not mapped for this binding. It most likely a bug.", simpleChannelUID); + } + } + + return data; + } + + @Override + protected void processCommand(LGThinQAbstractDeviceHandler.AsyncCommandParams params) throws LGThinqApiException { + WasherDryerSnapshot lastShot = getLastShot(); + Command command = params.command; + String simpleChannelUID; + simpleChannelUID = getSimpleChannelUID(params.channelUID); + switch (simpleChannelUID) { + case CHANNEL_WMD_REMOTE_START_START_STOP: { + if (command instanceof OnOffType) { + if (OnOffType.ON.equals(command)) { + if (!lastShot.isStandBy()) { + lgThinqWMApiClientService.remoteStart(getBridgeId(), getCapabilities(), getDeviceId(), + getRemoteStartData()); + } else { + logger.warn( + "WM is in StandBy mode. Command START can't be sent to Remote Start channel. Ignoring"); + } + } else { + logger.warn("Command Remote Start OFF not implemented yet"); + } + } else { + logger.warn("Received command different of StringType in Remote Start Channel. Ignoring"); + } + break; + } + case CHANNEL_WMD_STAND_BY_ID: { + if (command instanceof OnOffType) { + lgThinqWMApiClientService.wakeUp(getBridgeId(), getDeviceId(), OnOffType.ON.equals(command)); + } else { + logger.warn("Received command different of OnOffType in StandBy Channel. Ignoring"); + } + break; + } + default: { + logger.warn("Command {} to the channel {} not supported. Ignored.", command, params.channelUID); + } + } + } + + @Override + public String getDeviceAlias() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_DEVICE_ALIAS)); + } + + @Override + public String getDeviceUriJsonConfig() { + return emptyIfNull(getThing().getProperties().get(PROP_INFO_MODEL_URL_INFO)); + } + + @Override + public void onDeviceRemoved() { + } + + /** + * Put the channels in default state if the device is disconnected or gone. + */ + @Override + public void onDeviceDisconnected() { + updateState(CHANNEL_AC_POWER_ID, OnOffType.OFF); + updateState(CHANNEL_WMD_STATE_ID, new StringType(WMD_POWER_OFF_VALUE)); + updateState(CHANNEL_WMD_COURSE_ID, new StringType("NOT_SELECTED")); + updateState(CHANNEL_WMD_SMART_COURSE_ID, new StringType("NOT_SELECTED")); + updateState(CHANNEL_WMD_TEMP_LEVEL_ID, new StringType("NOT_SELECTED")); + updateState(CHANNEL_WMD_DOOR_LOCK_ID, new StringType("DOOR_LOCK_OFF")); + updateState(CHANNEL_WMD_REMAIN_TIME_ID, new StringType("00:00")); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGServicesConstants.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGServicesConstants.java new file mode 100644 index 0000000000000..e8b6cd40f8dd2 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGServicesConstants.java @@ -0,0 +1,245 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static java.util.Map.entry; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The LGServicesConstants constants class for lg services + * + * @author Nemer Daud - Initial Contribution + */ +@NonNullByDefault +public class LGServicesConstants { + // Extended Info Attribute Constants + public static final String CAP_EXTRA_ATTR_INSTANT_POWER = "InOutInstantPower"; + public static final String CAP_EXTRA_ATTR_FILTER_MAX_TIME_TO_USE = "ChangePeriod"; + public static final String CAP_EXTRA_ATTR_FILTER_USED_TIME = "UseTime"; + public static final String LG_ROOT_TAG_V1 = "lgedmRoot"; + public static final String LG_API_V1_CONTROL_OP = "rti/rtiControl"; + // === LG API protocol constants + public static final String LG_API_API_KEY_V2 = "VGhpblEyLjAgU0VSVklDRQ=="; + public static final String LG_API_APPLICATION_KEY = "6V1V8H2BN5P9ZQGOI5DAQ92YZBDO3EK9"; + public static final String LG_API_APP_LEVEL = "PRD"; + public static final String LG_API_APP_OS = "ANDROID"; + public static final String LG_API_APP_TYPE = "NUTS"; + public static final String LG_API_APP_VER = "3.5.1200"; + // the client id is a SHA512 hash of the phone MFR,MODEL,SERIAL, + // and the build id of the thinq app it can also just be a random + // string, we use the same client id used for oauth + public static final String LG_API_CLIENT_ID = "LGAO221A02"; + public static final String LG_API_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss +0000"; + public static final String LG_API_GATEWAY_SERVICE_PATH_V2 = "/v1/service/application/gateway-uri"; + public static final String LG_API_GATEWAY_URL_V2 = "https://route.lgthinq.com:46030" + + LG_API_GATEWAY_SERVICE_PATH_V2; + public static final String LG_API_MESSAGE_ID = "wideq"; + public static final String LG_API_OAUTH_CLIENT_KEY = "LGAO722A02"; + public static final String LG_API_OAUTH_SEARCH_KEY_PATH = "/searchKey"; + public static final String LG_API_OAUTH_SECRET_KEY = "c053c2a6ddeb7ad97cb0eed0dcb31cf8"; + public static final String LG_API_PLATFORM_TYPE_V1 = "thinq1"; + public static final String LG_API_PLATFORM_TYPE_V2 = "thinq2"; + public static final String LG_API_PRE_LOGIN_PATH = "/preLogin"; + public static final String LG_API_SECURITY_KEY = "nuts_securitykey"; + public static final String LG_API_SVC_CODE = "SVC202"; + public static final String LG_API_SVC_PHASE = "OP"; + public static final String LG_API_V1_MON_DATA_PATH = "rti/rtiResult"; + public static final String LG_API_V1_START_MON_PATH = "rti/rtiMon"; + public static final String LG_API_V2_API_KEY = "VGhpblEyLjAgU0VSVklDRQ=="; + public static final String LG_API_V2_APP_LEVEL = "PRD"; + public static final String LG_API_V2_APP_OS = "LINUX"; + public static final String LG_API_V2_APP_TYPE = "NUTS"; + public static final String LG_API_V2_APP_VER = "3.0.1700"; + public static final String LG_API_V2_AUTH_PATH = "/oauth/1.0/oauth2/token"; + public static final String LG_API_V2_CLIENT_ID = "65260af7e8e6547b51fdccf930097c51eb9885a508d3fddfa9ee6cdec22ae1bd"; + public static final String LG_API_V2_CTRL_DEVICE_CONFIG_PATH = "service/devices/%s/%s"; + public static final String LG_API_V2_DEVICE_CONFIG_PATH = "service/devices/"; + public static final String LG_API_V2_EMP_SESS_PATH = "/emp/oauth2/token/empsession"; + public static final String LG_API_V2_EMP_SESS_URL = "https://emp-oauth.lgecloud.com" + LG_API_V2_EMP_SESS_PATH; + public static final String LG_API_V2_LS_PATH = "/service/application/dashboard"; + public static final String LG_API_V2_SESSION_LOGIN_PATH = "/emp/v2.0/account/session/"; + public static final String LG_API_V2_SVC_PHASE = "OP"; + public static final String LG_API_V2_USER_INFO = "/users/profile"; + public static final Double FREEZER_TEMPERATURE_IGNORE_VALUE = 255.0; + public static final Double FRIDGE_TEMPERATURE_IGNORE_VALUE = 255.0; + public static final String RE_TEMP_UNIT_CELSIUS = "CELSIUS"; + public static final String RE_TEMP_UNIT_CELSIUS_SYMBOL = "°C"; + public static final Set RE_CELSIUS_UNIT_VALUES = Set.of("01", "1", "C", "CELSIUS", + RE_TEMP_UNIT_CELSIUS_SYMBOL); + public static final String RE_TEMP_UNIT_FAHRENHEIT = "FAHRENHEIT"; + public static final String RE_TEMP_UNIT_FAHRENHEIT_SYMBOL = "°F"; + public static final Map CAP_RE_TEMP_UNIT_V2_MAP = Map.of(RE_TEMP_UNIT_CELSIUS, + RE_TEMP_UNIT_CELSIUS_SYMBOL, RE_TEMP_UNIT_FAHRENHEIT, RE_TEMP_UNIT_FAHRENHEIT_SYMBOL); + public static final Set RE_FAHRENHEIT_UNIT_VALUES = Set.of("02", "2", "F", "FAHRENHEIT", + RE_TEMP_UNIT_FAHRENHEIT_SYMBOL); + public static final Set RE_DOOR_OPEN_VALUES = Set.of("1", "01", "OPEN"); + public static final Set RE_DOOR_CLOSE_VALUES = Set.of("0", "00", "CLOSE"); + public static final String RE_SNAPSHOT_NODE_V2 = "refState"; + public static final String RE_SET_CONTROL_COMMAND_NAME_V1 = "SetControl"; + public static final Map CAP_RE_SMART_SAVING_MODE = Map.of("@CP_TERM_USE_NOT_W", "Disabled", + "@RE_SMARTSAVING_MODE_NIGHT_W", "Night Mode", "@RE_SMARTSAVING_MODE_CUSTOM_W", "Custom Mode"); + public static final Map CAP_RE_ON_OFF = Map.of("@CP_OFF_EN_W", "Off", "@CP_ON_EN_W", "On"); + public static final Map CAP_RE_LABEL_ON_OFF = Map.of("OFF", "Off", "ON", "On", "IGNORE", + "Not Available"); + public static final Map CAP_RE_LABEL_CLOSE_OPEN = Map.of("CLOSE", "Closed", "OPEN", "Open", + "IGNORE", "Not Available"); + public static final Map CAP_RE_EXPRESS_FREEZE_MODES = Map.of("@CP_OFF_EN_W", "Express Mode Off", + "@CP_ON_EN_W", "Express Freeze On", "@RE_MAIN_SPEED_FREEZE_TERM_W", "Rapid Freeze On"); + public static final Map CAP_RE_FRESH_AIR_FILTER_MAP = Map.ofEntries(/* v1 */ entry("1", "Off"), + entry("2", "Auto Mode"), entry("3", "Power Mode"), entry("4", "Replace Filter"), + /* v2 */ entry("OFF", "Off"), entry("AUTO", "Auto Mode"), entry("POWER", "Power Mode"), + entry("REPLACE", "Replace Filter"), entry("SMART_STORAGE_POWER", "Smart Storage Power"), + entry("SMART_STORAGE_OFF", "Smart Storage Off"), entry("SMART_STORAGE_ON", "Smart Storage On"), + entry("IGNORE", "Not Available")); + public static final Map CAP_RE_SMART_SAVING_V2_MODE = Map.of("OFF", "Off", "NIGHT_ON", "Night Mode", + "CUSTOM_ON", "Custom Mode", "SMARTGRID_DR_ON", "Demand Response", "SMARTGRID_DD_ON", "Delay Defrost", + "IGNORE", "Not Available"); + public static final Map CAP_RE_WATER_FILTER = Map.ofEntries(entry("0_MONTH", "0 Month Used"), + entry("0", "0 Month Used"), entry("1_MONTH", "1 Month Used"), entry("1", "1 Month Used"), + entry("2_MONTH", "2 Month Used"), entry("2", "2 Month Used"), entry("3_MONTH", "3 Month Used"), + entry("3", "3 Month Used"), entry("4_MONTH", "4 Month Used"), entry("4", "4 Month Used"), + entry("5_MONTH", "5 Month Used"), entry("5", "5 Month Used"), entry("6_MONTH", "6 Month Used"), + entry("6", "6 Month Used"), entry("7_MONTH", "7 Month Used"), entry("8_MONTH", "8 Month Used"), + entry("9_MONTH", "9 Month Used"), entry("10_MONTH", "10 Month Used"), entry("11_MONTH", "11 Month Used"), + entry("12_MONTH", "12 Month Used"), entry("IGNORE", "Not Available")); + public static final String CAP_RE_WATER_FILTER_USED_POSTFIX = "Month(s) Used"; + // === Device Definition/Capability Constants + public static final String CAP_ACHP_OP_MODE_COOL_KEY = "@AC_MAIN_OPERATION_MODE_COOL_W"; + public static final String CAP_ACHP_OP_MODE_HEAT_KEY = "@AC_MAIN_OPERATION_MODE_HEAT_W"; + public static final Map CAP_AC_OP_MODE = Map.of(CAP_ACHP_OP_MODE_COOL_KEY, "Cool", + "@AC_MAIN_OPERATION_MODE_DRY_W", "Dry", "@AC_MAIN_OPERATION_MODE_FAN_W", "Fan", CAP_ACHP_OP_MODE_HEAT_KEY, + "Heat", "@AC_MAIN_OPERATION_MODE_AIRCLEAN_W", "Air Clean", "@AC_MAIN_OPERATION_MODE_ACO_W", "Auto", + "@AC_MAIN_OPERATION_MODE_AI_W", "AI", "@AC_MAIN_OPERATION_MODE_ENERGY_SAVING_W", "Eco", + "@AC_MAIN_OPERATION_MODE_AROMA_W", "Aroma", "@AC_MAIN_OPERATION_MODE_ANTIBUGS_W", "Anti Bugs"); + public static final Map CAP_AC_STEP_UP_DOWN_MODE = Map.of("@OFF", "Off", "@1", "Upper", "@2", "Up", + "@3", "Middle Up", "@4", "Middle Down", "@5", "Down", "@6", "Far Down", "@100", "Circular"); + public static final Map CAP_AC_STEP_LEFT_RIGHT_MODE = Map.of("@OFF", "Off", "@1", "Lefter", "@2", + "Left", "@3", "Middle", "@4", "Right", "@5", "Righter", "@13", "Left to Middle", "@35", "Middle to Right", + "@100", "Circular"); + // Sub Modes support + public static final String CAP_AC_SUB_MODE_COOL_JET = "@AC_MAIN_WIND_MODE_COOL_JET_W"; + public static final String CAP_AC_SUB_MODE_STEP_UP_DOWN = "@AC_MAIN_WIND_DIRECTION_STEP_UP_DOWN_W"; + public static final String CAP_AC_SUB_MODE_STEP_LEFT_RIGHT = "@AC_MAIN_WIND_DIRECTION_STEP_LEFT_RIGHT_W"; + public static final Map CAP_AC_FAN_SPEED = Map.ofEntries( + entry("@AC_MAIN_WIND_STRENGTH_SLOW_W", "Slow"), entry("@AC_MAIN_WIND_STRENGTH_SLOW_LOW_W", "Slower"), + entry("@AC_MAIN_WIND_STRENGTH_LOW_W", "Low"), entry("@AC_MAIN_WIND_STRENGTH_LOW_MID_W", "Low Mid"), + entry("@AC_MAIN_WIND_STRENGTH_MID_W", "Mid"), entry("@AC_MAIN_WIND_STRENGTH_MID_HIGH_W", "Mid High"), + entry("@AC_MAIN_WIND_STRENGTH_HIGH_W", "High"), entry("@AC_MAIN_WIND_STRENGTH_POWER_W", "Power"), + entry("@AC_MAIN_WIND_STRENGTH_AUTO_W", "Auto"), entry("@AC_MAIN_WIND_STRENGTH_NATURE_W", "Auto"), + entry("@AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W", "Right Low"), + entry("@AC_MAIN_WIND_STRENGTH_MID_RIGHT_W", "Right Mid"), + entry("@AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W", "Right High"), + entry("@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W", "Left Low"), + entry("@AC_MAIN_WIND_STRENGTH_MID_LEFT_W", "Left Mid"), + entry("@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W", "Left High")); + public static final Map CAP_AC_COOL_JET = Map.of("@COOL_JET", "Cool Jet"); + public static final Double CAP_HP_AIR_SWITCH = 0.0; + public static final Double CAP_HP_WATER_SWITCH = 1.0; + // ======= RAC MODES + public static final String CAP_AC_AUTODRY = "@AUTODRY"; + public static final String CAP_AC_ENERGYSAVING = "@ENERGYSAVING"; + public static final String CAP_AC_AIRCLEAN = "@AIRCLEAN"; + // ==================== + public static final String CAP_AC_COMMAND_OFF = "@OFF"; + public static final String CAP_AC_COMMAND_ON = "@ON"; + public static final String CAP_AC_AIR_CLEAN_COMMAND_ON = "@AC_MAIN_AIRCLEAN_ON_W"; + public static final String CAP_AC_AIR_CLEAN_COMMAND_OFF = "@AC_MAIN_AIRCLEAN_OFF_W"; + public static final String WMD_COURSE_NOT_SELECTED_VALUE = "NOT_SELECTED"; + public static final String WMD_POWER_OFF_VALUE = "POWEROFF"; + public static final String WMD_SNAPSHOT_WASHER_DRYER_NODE_V2 = "washerDryer"; + public static final String WM_LOST_WASHING_STATE_KEY = "WASHING"; + public static final String WM_LOST_WASHING_STATE_VALUE = "@WM_STATE_WASHING_W"; + public static final Map CAP_WMD_STATE = Map.ofEntries(entry("@WM_STATE_POWER_OFF_W", "Off"), + entry("@WM_STATE_INITIAL_W", "Initial"), entry("@WM_STATE_PAUSE_W", "Pause"), + entry("@WM_STATE_RESERVE_W", "Reserved"), entry("@WM_STATE_DETECTING_W", "Detecting"), + entry("@WM_STATE_RUNNING_W", "Running"), entry("@WM_STATE_RINSING_W", "Rinsing"), + entry("@WM_STATE_SPINNING_W", "Spinning"), entry("@WM_STATE_COOLDOWN_W", "Cool Down"), + entry("@WM_STATE_RINSEHOLD_W", "Rinse Hold"), entry("@WM_STATE_WASH_REFRESHING_W", "Refreshing"), + entry("@WM_STATE_STEAMSOFTENING_W", "Steam Softening"), entry("@WM_STATE_END_W", "End"), + entry("@WM_STATE_DRYING_W", "Drying"), entry("@WM_STATE_DEMO_W", "Demonstration"), + entry("@WM_STATE_ADD_DRAIN_W", "Add Drain"), entry("@WM_STATE_LOAD_DISPLAY_W", "Loading Display"), + entry("@WM_STATE_FRESHCARE_W", "Refreshing"), entry("@WM_STATE_ERROR_AUTO_OFF_W", "Error Auto Off"), + entry("@WM_STATE_FROZEN_PREVENT_INITIAL_W", "Frozen Preventing"), + entry("@FROZEN_PREVENT_PAUSE", "Frozen Preventing Paused"), + entry("@FROZEN_PREVENT_RUNNING", "Frozen Preventing Running"), entry("@AUDIBLE_DIAGNOSIS", "Diagnosing"), + entry("@WM_STATE_ERROR_W", "Error"), + // This last one is not defined in the cap file + entry(WM_LOST_WASHING_STATE_VALUE, "Washing")); + public static final Map CAP_WMD_PROCESS_STATE = Map.ofEntries( + entry("@WM_STATE_DETECTING_W", "Detecting"), entry("@WM_STATE_STEAM_W", "Steam"), + entry("@WM_STATE_DRY_W", "Drying"), entry("@WM_STATE_COOLING_W", "Cooling"), + entry("@WM_STATE_ANTI_CREASE_W", "Anti Creasing"), entry("@WM_STATE_END_W", "End"), + entry("@WM_STATE_POWER_OFF_W", "Power Off"), entry("@WM_STATE_INITIAL_W", "Initializing"), + entry("@WM_STATE_PAUSE_W", "Paused"), entry("@WM_STATE_RESERVE_W", "Reserved"), + entry("@WM_STATE_RUNNING_W", "Running"), entry("@WM_STATE_RINSING_W", "Rising"), + entry("@WM_STATE_SPINNING_W", "@WM_STATE_DRYING_W"), entry("WM_STATE_COOLDOWN_W", "Cool Down"), + entry("@WM_STATE_RINSEHOLD_W", "Rinse Hold"), entry("@WM_STATE_WASH_REFRESHING_W", "Refreshing"), + entry("@WM_STATE_STEAMSOFTENING_W", "Steam Softening"), entry("@WM_STATE_ERROR_W", "Error")); + public static final Map CAP_DR_DRY_LEVEL = Map.ofEntries( + entry("@WM_DRY24_DRY_LEVEL_IRON_W", "Iron"), entry("@WM_DRY24_DRY_LEVEL_CUPBOARD_W", "Cupboard"), + entry("@WM_DRY24_DRY_LEVEL_EXTRA_W", "Extra")); + public static final Map CAP_WMD_TEMPERATURE = Map.ofEntries( + entry("@WM_TERM_NO_SELECT_W", "Not Selected"), entry("@WM_TITAN2_OPTION_TEMP_20_W", "20"), + entry("@WM_TITAN2_OPTION_TEMP_COLD_W", "Cold"), entry("@WM_TITAN2_OPTION_TEMP_30_W", "30"), + entry("@WM_TITAN2_OPTION_TEMP_40_W", "40"), entry("@WM_TITAN2_OPTION_TEMP_50_W", "50"), + entry("@WM_TITAN27_BIG_OPTION_TEMP_TAP_COLD_W", "Tap Cold"), + entry("@WM_TITAN27_BIG_OPTION_TEMP_COLD_W", "Cold"), + entry("@WM_TITAN27_BIG_OPTION_TEMP_ECO_WARM_W", "Eco Warm"), + entry("@WM_TITAN27_BIG_OPTION_TEMP_WARM_W", "Warm"), entry("@WM_TITAN27_BIG_OPTION_TEMP_HOT_W", "Hot"), + entry("@WM_TITAN27_BIG_OPTION_TEMP_EXTRA_HOT_W", "Extra Hot")); + public static final Map CAP_WM_SPIN = Map.ofEntries(entry("@WM_TERM_NO_SELECT_W", "Not Selected"), + entry("@WM_TITAN2_OPTION_SPIN_NO_SPIN_W", "No Spin"), entry("@WM_TITAN2_OPTION_SPIN_400_W", "400"), + entry("@WM_TITAN2_OPTION_SPIN_600_W", "600"), entry("@WM_TITAN2_OPTION_SPIN_700_W", "700"), + entry("@WM_TITAN2_OPTION_SPIN_800_W", "800"), entry("@WM_TITAN2_OPTION_SPIN_900_W", "900"), + entry("@WM_TITAN2_OPTION_SPIN_1000_W", "1000"), entry("@WM_TITAN2_OPTION_SPIN_1100_W", "1100"), + entry("@WM_TITAN2_OPTION_SPIN_1200_W", "1200"), entry("@WM_TITAN2_OPTION_SPIN_1400_W", "1400"), + entry("@WM_TITAN2_OPTION_SPIN_1600_W", "1600"), entry("@WM_TITAN2_OPTION_SPIN_MAX_W", "Max Spin"), + entry("@WM_TITAN27_BIG_OPTION_SPIN_NO_SPIN_W", "Drain Only"), + entry("@WM_TITAN27_BIG_OPTION_SPIN_LOW_W", "Low"), entry("@WM_TITAN27_BIG_OPTION_SPIN_MEDIUM_W", "Medium"), + entry("@WM_TITAN27_BIG_OPTION_SPIN_HIGH_W", "High"), + entry("@WM_TITAN27_BIG_OPTION_SPIN_EXTRA_HIGH_W", "Extra High")); + public static final Map CAP_WM_RINSE = Map.ofEntries(entry("@WM_TERM_NO_SELECT_W", "Not Selected"), + entry("@WM_TITAN2_OPTION_RINSE_NORMAL_W", "Normal"), entry("@WM_TITAN2_OPTION_RINSE_RINSE+_W", "Plus"), + entry("@WM_TITAN2_OPTION_RINSE_RINSE++_W", "Plus +"), + entry("@WM_TITAN2_OPTION_RINSE_NORMALHOLD_W", "Normal Hold"), + entry("@WM_TITAN2_OPTION_RINSE_RINSE+HOLD_W", "Plus Hold"), + entry("@WM_TITAN27_BIG_OPTION_EXTRA_RINSE_0_W", "Normal"), + entry("@WM_TITAN27_BIG_OPTION_EXTRA_RINSE_1_W", "Plus"), + entry("@WM_TITAN27_BIG_OPTION_EXTRA_RINSE_2_W", "Plus +"), + entry("@WM_TITAN27_BIG_OPTION_EXTRA_RINSE_3_W", "Plus ++")); + // This is the dictionary os course functions translations for V2 + public static final Map> CAP_WM_DICT_V2 = Map.of("spin", CAP_WM_SPIN, "rinse", + CAP_WM_RINSE, "temp", CAP_WMD_TEMPERATURE, "state", CAP_WMD_STATE); + public static final String WMD_COMMAND_REMOTE_START_V2 = "WMStart"; + /** + * ============ Dish Washer's Label/Feature Translation Constants ============= + */ + public static final String DW_SNAPSHOT_WASHER_DRYER_NODE_V2 = "dishwasher"; + public static final String DW_POWER_OFF_VALUE = "POWEROFF"; + public static final String DW_STATE_COMPLETE = "END"; + public static final Map CAP_DW_DOOR_STATE = Map.of("@CP_OFF_EN_W", "Close", "@CP_ON_EN_W", + "Opened"); + public static final Map CAP_DW_PROCESS_STATE = Map.ofEntries(entry("@DW_STATE_INITIAL_W", "None"), + entry("@DW_STATE_RESERVE_W", "Reserved"), entry("@DW_STATE_RUNNING_W", "Running"), + entry("@DW_STATE_RINSING_W", "Rising"), entry("@DW_STATE_DRYING_W", "Drying"), + entry("@DW_STATE_COMPLETE_W", "Complete"), entry("@DW_STATE_NIGHTDRY_W", "Night Dry"), + entry("@DW_STATE_CANCEL_W", "Cancelled")); + public static final Map CAP_DW_STATE = Map.ofEntries(entry("@DW_STATE_POWER_OFF_W", "Off"), + entry("@DW_STATE_INITIAL_W", "Initial"), entry("@DW_STATE_RUNNING_W", "Running"), + entry("@DW_STATE_PAUSE_W", "Paused"), entry("@DW_STATE_STANDBY_W", "Stand By"), + entry("@DW_STATE_COMPLETE_W", "Complete"), entry("@DW_STATE_POWER_FAIL_W", "Power Fail")); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiClientService.java new file mode 100644 index 0000000000000..f2c74224aba2d --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiClientService.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACTargetTmp; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ExtendedDeviceInfo; + +/** + * The {@link LGThinQACApiClientService} - Common interface to be used by the AC Handle to access LG API Services in V1 + * & v2 + * protocol versions + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface LGThinQACApiClientService extends LGThinQApiClientService { + /** + * Change AC Operation Mode (Cool, Heat, etc.) + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param newOpMode - The new operation mode to be setup + * @throws LGThinqApiException - If some error invoking LG API. + */ + void changeOperationMode(String bridgeName, String deviceId, int newOpMode) throws LGThinqApiException; + + /** + * Change the AC Fan Speed. + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param newFanSpeed - new Fan Speed to be setup + * @throws LGThinqApiException - If some error invoking LG API. + */ + void changeFanSpeed(String bridgeName, String deviceId, int newFanSpeed) throws LGThinqApiException; + + /** + * Change the fan vertical orientation + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param currentSnap - Current data snapshot + * @param newStep - new vertical position + * @throws LGThinqApiException - If some error invoking LG API. + */ + void changeStepUpDown(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep) + throws LGThinqApiException; + + /** + * Change the fan horizontal orientation + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param currentSnap - Current data snapshot + * @param newStep - new horizontal position + * @throws LGThinqApiException - If some error invoking LG API. + */ + void changeStepLeftRight(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep) + throws LGThinqApiException; + + /** + * Change the target temperature + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param newTargetTemp - new target temperature + * @throws LGThinqApiException - If some error invoking LG API. + */ + void changeTargetTemperature(String bridgeName, String deviceId, ACTargetTmp newTargetTemp) + throws LGThinqApiException; + + /** + * Turn On/Off the Jet Mode feature + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param modeOnOff - turn on/off + * @throws LGThinqApiException - If some error invoking LG API. + */ + void turnCoolJetMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException; + + /** + * Turn On/Off the Air Clean feature + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param modeOnOff - turn on/off + * @throws LGThinqApiException - If some error invoking LG API. + */ + void turnAirCleanMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException; + + /** + * Turn On/Off the Auto Dry feature + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param modeOnOff - turn on/off + * @throws LGThinqApiException - If some error invoking LG API. + */ + void turnAutoDryMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException; + + /** + * Turn On/Off the Energy Saving feature + * + * @param bridgeName - name of the bridge + * @param deviceId - ID of the LG Thinq Device + * @param modeOnOff - turn on/off + * @throws LGThinqApiException - If some error invoking LG API. + */ + void turnEnergySavingMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException; + + /** + * Get Extended Device Information (Energy consumption, filter level, etc). + * + * @param bridgeName Bridge name + * @param deviceId - ID of the LG Thinq Device + * @return ExtendedDeviceInfo containing the device extended data + * @throws LGThinqApiException - If some error invoking LG API. + */ + ExtendedDeviceInfo getExtendedDeviceInfo(String bridgeName, String deviceId) throws LGThinqApiException; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiV1ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiV1ClientServiceImpl.java new file mode 100644 index 0000000000000..28ace60b90d1a --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiV1ClientServiceImpl.java @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V1_CONTROL_OP; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_ROOT_TAG_V1; + +import java.io.IOException; +import java.util.Base64; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACTargetTmp; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ExtendedDeviceInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link LGThinQACApiV1ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQACApiV1ClientServiceImpl extends + LGThinQAbstractApiV1ClientService implements LGThinQACApiClientService { + + private static final Logger logger = LoggerFactory.getLogger(LGThinQACApiV1ClientServiceImpl.class); + + protected LGThinQACApiV1ClientServiceImpl(HttpClient httpClient) { + super(ACCapability.class, ACCanonicalSnapshot.class, httpClient); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + // there's no before settings to send command + return false; + } + + /** + * Get snapshot data from the device. + * It works only for API V2 device versions! + * + * @param deviceId device ID for de desired V2 LG Thinq. + * @param capDef + * @return return map containing metamodel of settings and snapshot + */ + @Override + @Nullable + public ACCanonicalSnapshot getDeviceData(String bridgeName, String deviceId, CapabilityDefinition capDef) { + throw new UnsupportedOperationException("Method not supported in V1 API device."); + } + + private void readDataResultNodeToObject(String jsonResult, Object obj) throws IOException { + JsonNode node = objectMapper.readTree(jsonResult); + JsonNode data = node.path(LG_ROOT_TAG_V1).path("returnData"); + if (data.isTextual()) { + // analyses if its b64 or not + JsonNode format = node.path(LG_ROOT_TAG_V1).path("format"); + if ("B64".equals(format.textValue())) { + String dataStr = new String(Base64.getDecoder().decode(data.textValue())); + objectMapper.readerForUpdating(obj).readValue(dataStr); + } else { + objectMapper.readerForUpdating(obj).readValue(data.textValue()); + } + } else { + logger.warn("Data returned by LG API to get energy state is not present. Result:{}", node.toPrettyString()); + } + } + + @Override + public ExtendedDeviceInfo getExtendedDeviceInfo(String bridgeName, String deviceId) throws LGThinqApiException { + ExtendedDeviceInfo info = new ExtendedDeviceInfo(); + try { + RestResult resp = sendCommand(bridgeName, deviceId, LG_API_V1_CONTROL_OP, "Config", "Get", "", + "InOutInstantPower"); + handleGenericErrorResult(resp); + readDataResultNodeToObject(resp.getJsonResponse(), info); + + resp = sendCommand(bridgeName, deviceId, LG_API_V1_CONTROL_OP, "Config", "Get", "", "Filter"); + handleGenericErrorResult(resp); + readDataResultNodeToObject(resp.getJsonResponse(), info); + + return info; + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error sending command to LG API", e); + } + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) + throws LGThinqApiException { + try { + RestResult resp = sendCommand(bridgeName, deviceId, "", "Control", "Set", "Operation", + String.valueOf(newPowerState.commandValue())); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting device power", e); + } + } + + @Override + public void turnCoolJetMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "Jet", modeOnOff); + } + + public void turnAirCleanMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "AirClean", modeOnOff); + } + + public void turnAutoDryMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "AutoDry", modeOnOff); + } + + public void turnEnergySavingMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "PowerSave", modeOnOff); + } + + protected void turnGenericMode(String bridgeName, String deviceId, String modeName, String modeOnOff) + throws LGThinqApiException { + try { + RestResult resp = sendCommand(bridgeName, deviceId, "", "Control", "Set", modeName, modeOnOff); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting " + modeName + " mode", e); + } + } + + @Override + public void changeOperationMode(String bridgeName, String deviceId, int newOpMode) throws LGThinqApiException { + try { + RestResult resp = sendCommand(bridgeName, deviceId, "", "Control", "Set", "OpMode", "" + newOpMode); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting operation mode", e); + } + } + + @Override + public void changeFanSpeed(String bridgeName, String deviceId, int newFanSpeed) throws LGThinqApiException { + try { + RestResult resp = sendCommand(bridgeName, deviceId, "", "Control", "Set", "WindStrength", + String.valueOf(newFanSpeed)); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting fan speed", e); + } + } + + @Override + public void changeStepUpDown(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep) + throws LGThinqApiException { + Map<@Nullable String, @Nullable Object> subModeFeatures = Map.of("Jet", currentSnap.getCoolJetMode().intValue(), + "PowerSave", currentSnap.getEnergySavingMode().intValue(), "WDirVStep", newStep, "WDirHStep", + (int) currentSnap.getStepLeftRightMode()); + try { + RestResult resp = sendCommand(bridgeName, deviceId, "", "Control", "Set", subModeFeatures, null); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error stepUpDown", e); + } + } + + @Override + public void changeStepLeftRight(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep) + throws LGThinqApiException { + Map<@Nullable String, @Nullable Object> subModeFeatures = Map.of("Jet", currentSnap.getCoolJetMode().intValue(), + "PowerSave", currentSnap.getEnergySavingMode().intValue(), "WDirVStep", + (int) currentSnap.getStepUpDownMode(), "WDirHStep", newStep); + try { + RestResult resp = sendCommand(bridgeName, deviceId, "", "Control", "Set", subModeFeatures, null); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error stepUpDown", e); + } + } + + @Override + public void changeTargetTemperature(String bridgeName, String deviceId, ACTargetTmp newTargetTemp) + throws LGThinqApiException { + try { + RestResult resp = sendCommand(bridgeName, deviceId, "", "Control", "Set", "TempCfg", + String.valueOf(newTargetTemp.commandValue())); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting target temperature", e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiV2ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiV2ClientServiceImpl.java new file mode 100644 index 0000000000000..585bce9d1dd27 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiV2ClientServiceImpl.java @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACTargetTmp; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ExtendedDeviceInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * The {@link LGThinQACApiV2ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQACApiV2ClientServiceImpl extends + LGThinQAbstractApiV2ClientService implements LGThinQACApiClientService { + + private static final Logger logger = LoggerFactory.getLogger(LGThinQACApiV2ClientServiceImpl.class); + + protected LGThinQACApiV2ClientServiceImpl(HttpClient httpClient) { + super(ACCapability.class, ACCanonicalSnapshot.class, httpClient); + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) + throws LGThinqApiException { + try { + RestResult resp = sendBasicControlCommands(bridgeName, deviceId, "Operation", "airState.operation", + newPowerState.commandValue()); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting device power", e); + } + } + + @Override + public void turnCoolJetMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "airState.wMode.jet", modeOnOff); + } + + public void turnAirCleanMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "airState.wMode.airClean", modeOnOff); + } + + public void turnAutoDryMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "airState.miscFuncState.autoDry", modeOnOff); + } + + public void turnEnergySavingMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException { + turnGenericMode(bridgeName, deviceId, "airState.powerSave.basic", modeOnOff); + } + + protected void turnGenericMode(String bridgeName, String deviceId, String modeName, String modeOnOff) + throws LGThinqApiException { + try { + RestResult resp = sendBasicControlCommands(bridgeName, deviceId, "Operation", modeName, + Integer.parseInt(modeOnOff)); + handleGenericErrorResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting cool jet mode", e); + } + } + + @Override + public void changeOperationMode(String bridgeName, String deviceId, int newOpMode) throws LGThinqApiException { + try { + RestResult resp = sendBasicControlCommands(bridgeName, deviceId, "Set", "airState.opMode", newOpMode); + handleGenericErrorResult(resp); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting operation mode", e); + } + } + + @Override + public void changeFanSpeed(String bridgeName, String deviceId, int newFanSpeed) throws LGThinqApiException { + try { + RestResult resp = sendBasicControlCommands(bridgeName, deviceId, "Set", "airState.windStrength", + newFanSpeed); + handleGenericErrorResult(resp); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting operation mode", e); + } + } + + @Override + public void changeStepUpDown(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep) + throws LGThinqApiException { + try { + RestResult resp = sendBasicControlCommands(bridgeName, deviceId, "Set", "airState.wDir.vStep", newStep); + handleGenericErrorResult(resp); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting operation mode", e); + } + } + + @Override + public void changeStepLeftRight(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep) + throws LGThinqApiException { + try { + RestResult resp = sendBasicControlCommands(bridgeName, deviceId, "Set", "airState.wDir.hStep", newStep); + handleGenericErrorResult(resp); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting operation mode", e); + } + } + + @Override + public void changeTargetTemperature(String bridgeName, String deviceId, ACTargetTmp newTargetTemp) + throws LGThinqApiException { + try { + RestResult resp = sendBasicControlCommands(bridgeName, deviceId, "Set", "airState.tempState.target", + newTargetTemp.commandValue()); + handleGenericErrorResult(resp); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error adjusting operation mode", e); + } + } + + /** + * Start monitor data form specific device. This is old one, works only on V1 API supported devices. + * + * @param deviceId Device ID + * @return Work1 to be uses to grab data during monitoring. + */ + @Override + public String startMonitor(String bridgeName, String deviceId) { + throw new UnsupportedOperationException("Not supported in V2 API."); + } + + @Override + public void stopMonitor(String bridgeName, String deviceId, String workId) { + throw new UnsupportedOperationException("Not supported in V2 API."); + } + + @Override + public @Nullable ACCanonicalSnapshot getMonitorData(String bridgeName, String deviceId, String workId, + DeviceTypes deviceType, ACCapability deviceCapability) { + throw new UnsupportedOperationException("Not supported in V2 API."); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + try { + RestResult resp = sendCommand(bridgeName, deviceId, "control", "allEventEnable", "Set", + "airState.mon.timeout", "70"); + handleGenericErrorResult(resp); + if (resp.getStatusCode() == 400) { + // Access Denied. Return false to indicate user don't have access to this functionality + return false; + } + } catch (Exception e) { + logger.debug("Can't execute Before Update command", e); + } + return true; + } + + /** + * Expect receiving json of format: { + * ... + * result: { + * data: { + * ... + * } + * ... + * } + * } + * Data node will be deserialized into the object informed + * + * @param jsonResult json result + * @param obj object to be updated + * @throws IOException if there are errors deserialization the jsonResult + */ + private void readDataResultNodeToObject(String jsonResult, Object obj) throws IOException { + JsonNode node = objectMapper.readTree(jsonResult); + JsonNode data = node.path("result").path("data"); + if (data.isObject()) { + objectMapper.readerForUpdating(obj).readValue(data); + } else { + logger.warn("Data returned by LG API to get energy state is not present. Result:{}", node.toPrettyString()); + } + } + + @Override + public ExtendedDeviceInfo getExtendedDeviceInfo(String bridgeName, String deviceId) throws LGThinqApiException { + ExtendedDeviceInfo info = new ExtendedDeviceInfo(); + try { + ObjectNode dataList = JsonNodeFactory.instance.objectNode(); + dataList.put("dataGetList", (Integer) null); + dataList.put("dataSetList", (Integer) null); + + RestResult resp = sendCommand(bridgeName, deviceId, "control-sync", "energyStateCtrl", "Get", + "airState.energy.totalCurrent", "null", dataList); + handleGenericErrorResult(resp); + readDataResultNodeToObject(resp.getJsonResponse(), info); + + ObjectNode dataGetList = JsonNodeFactory.instance.objectNode(); + dataGetList.putArray("dataGetList").add("airState.filterMngStates.useTime") + .add("airState.filterMngStates.maxTime"); + resp = sendCommand(bridgeName, deviceId, "control-sync", "filterMngStateCtrl", "Get", null, null, + dataGetList); + handleGenericErrorResult(resp); + readDataResultNodeToObject(resp.getJsonResponse(), info); + + return info; + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error sending command to LG API: " + e.getMessage(), e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiClientService.java new file mode 100644 index 0000000000000..5cf920886d300 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiClientService.java @@ -0,0 +1,494 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.BASE_CAP_CONFIG_DATA_FILE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_SECURITY_KEY; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_SVC_CODE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V1_MON_DATA_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V1_START_MON_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_API_KEY; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_APP_LEVEL; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_APP_OS; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_APP_TYPE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_APP_VER; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_CLIENT_ID; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_DEVICE_CONFIG_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_LS_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_SVC_PHASE; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.internal.LGThinQBindingConstants; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.api.RestUtils; +import org.openhab.binding.lgthinq.lgservices.api.TokenManager; +import org.openhab.binding.lgthinq.lgservices.api.TokenResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqAccessException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqDeviceV1MonitorExpiredException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqDeviceV1OfflineException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityFactory; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.LGDevice; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringResultFormat; +import org.openhab.binding.lgthinq.lgservices.model.ResultCodes; +import org.openhab.binding.lgthinq.lgservices.model.SnapshotBuilderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * The {@link LGThinQAbstractApiClientService} - base class for all LG API client service. It's provide commons methods + * to + * communicate to the LG Cloud and exchange basic data. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@SuppressWarnings("unchecked") +public abstract class LGThinQAbstractApiClientService + implements LGThinQApiClientService { + private static final Logger logger = LoggerFactory.getLogger(LGThinQAbstractApiClientService.class); + protected final ObjectMapper objectMapper = new ObjectMapper(); + protected final TokenManager tokenManager; + protected final Class capabilityClass; + protected final Class snapshotClass; + protected final HttpClient httpClient; + + protected LGThinQAbstractApiClientService(Class capabilityClass, Class snapshotClass, HttpClient httpClient) { + this.httpClient = httpClient; + this.tokenManager = new TokenManager(httpClient); + this.capabilityClass = capabilityClass; + this.snapshotClass = snapshotClass; + } + + static Map getCommonHeaders(String language, String country, String accessToken, + String userNumber) { + Map headers = new HashMap<>(); + headers.put("Accept", "application/json"); + headers.put("Content-type", "application/json;charset=UTF-8"); + headers.put("x-api-key", LG_API_V2_API_KEY); + headers.put("x-client-id", LG_API_V2_CLIENT_ID); + headers.put("x-country-code", country); + headers.put("x-language-code", language); + headers.put("x-message-id", UUID.randomUUID().toString()); + headers.put("x-service-code", LG_API_SVC_CODE); + headers.put("x-service-phase", LG_API_V2_SVC_PHASE); + headers.put("x-thinq-app-level", LG_API_V2_APP_LEVEL); + headers.put("x-thinq-app-os", LG_API_V2_APP_OS); + headers.put("x-thinq-app-type", LG_API_V2_APP_TYPE); + headers.put("x-thinq-app-ver", LG_API_V2_APP_VER); + headers.put("x-thinq-security-key", LG_API_SECURITY_KEY); + if (!accessToken.isBlank()) + headers.put("x-emp-token", accessToken); + if (!userNumber.isBlank()) + headers.put("x-user-no", userNumber); + return headers; + } + + /** + * Even using V2 URL, this endpoint support grab informations about account devices from V1 and V2. + * + * @return list os LG Devices. + * @throws LGThinqApiException if some communication error occur. + */ + @Override + public List listAccountDevices(String bridgeName) throws LGThinqApiException { + try { + TokenResult token = tokenManager.getValidRegisteredToken(bridgeName); + UriBuilder builder = UriBuilder.fromUri(token.getGatewayInfo().getApiRootV2()).path(LG_API_V2_LS_PATH); + Map headers = getCommonHeaders(token.getGatewayInfo().getLanguage(), + token.getGatewayInfo().getCountry(), token.getAccessToken(), token.getUserInfo().getUserNumber()); + RestResult resp = RestUtils.getCall(httpClient, builder.build().toURL().toString(), headers, null); + return handleListAccountDevicesResult(resp); + } catch (Exception e) { + throw new LGThinqApiException("Error listing account devices from LG Server API", e); + } + } + + @Override + public File loadDeviceCapability(String deviceId, String uri, boolean forceRecreate) throws LGThinqApiException { + File regFile = new File(String.format(BASE_CAP_CONFIG_DATA_FILE, deviceId)); + try { + if (!regFile.isFile() || forceRecreate) { + try (InputStream in = new URL(uri).openStream()) { + Files.copy(in, regFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + } catch (IOException e) { + throw new LGThinqApiException("Error reading IO interface", e); + } + return regFile; + } + + /** + * Get device settings and snapshot for a specific device. + * It works only for API V2 device versions! + * + * @param deviceId device ID for de desired V2 LG Thinq. + * @return return map containing metamodel of settings and snapshot + * @throws LGThinqApiException if some communication error occur. + */ + @Override + public Map getDeviceSettings(String bridgeName, String deviceId) throws LGThinqApiException { + try { + TokenResult token = tokenManager.getValidRegisteredToken(bridgeName); + UriBuilder builder = UriBuilder.fromUri(token.getGatewayInfo().getApiRootV2()) + .path(String.format("%s/%s", LG_API_V2_DEVICE_CONFIG_PATH, deviceId)); + Map headers = getCommonHeaders(token.getGatewayInfo().getLanguage(), + token.getGatewayInfo().getCountry(), token.getAccessToken(), token.getUserInfo().getUserNumber()); + RestResult resp = RestUtils.getCall(httpClient, builder.build().toURL().toString(), headers, null); + return handleDeviceSettingsResult(resp); + } catch (LGThinqException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Errors list account devices from LG Server API", e); + } + } + + private Map handleDeviceSettingsResult(RestResult resp) throws LGThinqApiException { + return genericHandleDeviceSettingsResult(resp, objectMapper); + } + + static Map genericHandleDeviceSettingsResult(RestResult resp, ObjectMapper objectMapper) + throws LGThinqApiException { + Map deviceSettings; + Map respMap; + String resultCode; + if (resp.getStatusCode() != 200) { + if (resp.getStatusCode() == 400) { + LGThinQAbstractApiClientService.logger.warn( + "Error calling device settings from LG Server API. HTTP Status: {}. The reason is: {}", + resp.getStatusCode(), ResultCodes.getReasonResponse(resp.getJsonResponse())); + throw new LGThinqAccessException(String.format("Error calling device settings from LG Server API. HTTP Status: %d. The reason is: %s", + resp.getStatusCode(), ResultCodes.getReasonResponse(resp.getJsonResponse()))); + } + try { + respMap = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + resultCode = respMap.get("resultCode"); + if (resultCode != null) { + throw new LGThinqApiException(String.format( + "Error calling device settings from LG Server API. The code is: %s and The reason is: %s", + resultCode, ResultCodes.fromCode(resultCode))); + } + } catch (JsonProcessingException e) { + // This exception doesn't matter, it's because response is not in json format. Logging raw response. + } + throw new LGThinqApiException(String.format( + "Error calling device settings from LG Server API. The reason is:%s", resp.getJsonResponse())); + + } else { + try { + deviceSettings = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + String code = Objects.requireNonNullElse((String) deviceSettings.get("resultCode"), ""); + if (!ResultCodes.OK.containsResultCode(code)) { + throw new LGThinqApiException(String.format( + "LG API report error processing the request -> resultCode=[{%s], message=[%s]", code, + getErrorCodeMessage(code))); + } + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unknown error occurred deserializing json stream", e); + } + + } + return Objects.requireNonNull((Map) deviceSettings.get("result"), + "Unexpected json result asking for Device Settings. Node 'result' no present"); + } + + private List handleListAccountDevicesResult(RestResult resp) throws LGThinqApiException { + Map devicesResult; + List devices; + if (resp.getStatusCode() != 200) { + if (resp.getStatusCode() == 400) { + logger.warn("Error calling device list from LG Server API. HTTP Status: {}. The reason is: {}", + resp.getStatusCode(), ResultCodes.getReasonResponse(resp.getJsonResponse())); + return Collections.emptyList(); + } + throw new LGThinqApiException( + String.format("Error calling device list from LG Server API. HTTP Status: %s. The reason is: %s", + resp.getStatusCode(), ResultCodes.getReasonResponse(resp.getJsonResponse()))); + } else { + try { + devicesResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + String code = Objects.requireNonNullElse((String) devicesResult.get("resultCode"), ""); + if (!ResultCodes.OK.containsResultCode(code)) { + throw new LGThinqApiException( + String.format("LG API report error processing the request -> resultCode=[%s], message=[%s]", + code, getErrorCodeMessage(code))); + } + List> items = (List>) ((Map) Objects + .requireNonNull(devicesResult.get("result"), "Not expected null here")).get("item"); + devices = objectMapper.convertValue(items, new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unknown error occurred deserializing json stream.", e); + } + + } + + return devices; + } + + protected static String getErrorCodeMessage(@Nullable String code) { + if (code == null) { + return ""; + } + ResultCodes resultCode = ResultCodes.fromCode(code); + return resultCode.getDescription(); + } + + /** + * Get capability em registry/cache on file for next consult + * + * @param deviceId ID of the device + * @param uri URI of the config capability + * @return return simplified capability + * @throws LGThinqApiException If some error occurr + */ + public C getCapability(String deviceId, String uri, boolean forceRecreate) throws LGThinqApiException { + try { + File regFile = loadDeviceCapability(deviceId, uri, forceRecreate); + JsonNode rootNode = objectMapper.readTree(regFile); + return CapabilityFactory.getInstance().create(rootNode, capabilityClass); + } catch (IOException e) { + throw new LGThinqApiException("Error reading IO interface", e); + } catch (LGThinqException e) { + throw new LGThinqApiException("Error parsing capability registry", e); + } + } + + public S buildDefaultOfflineSnapshot() { + try { + // As I don't know the current device status, then I reset to default values. + @SuppressWarnings("null") + S shot = snapshotClass.getDeclaredConstructor().newInstance(); + shot.setPowerStatus(DevicePowerState.DV_POWER_OFF); + shot.setOnline(false); + return shot; + } catch (Exception ex) { + throw new IllegalStateException("Unexpected Error. The default constructor of this Snapshot wasn't found", + ex); + } + } + + public @Nullable S getMonitorData(String bridgeName, String deviceId, String workId, DeviceTypes deviceType, + C deviceCapability) throws LGThinqApiException, LGThinqDeviceV1MonitorExpiredException, IOException, + LGThinqUnmarshallException { + TokenResult token = tokenManager.getValidRegisteredToken(bridgeName); + UriBuilder builder = UriBuilder.fromUri(token.getGatewayInfo().getApiRootV1()).path(LG_API_V1_MON_DATA_PATH); + Map headers = getCommonHeaders(token.getGatewayInfo().getLanguage(), + token.getGatewayInfo().getCountry(), token.getAccessToken(), token.getUserInfo().getUserNumber()); + String jsonData = String.format("{\n" + " \"lgedmRoot\":{\n" + " \"workList\":[\n" + " {\n" + + " \"deviceId\":\"%s\",\n" + " \"workId\":\"%s\"\n" + " }\n" + + " ]\n" + " }\n" + "}", deviceId, workId); + RestResult resp = RestUtils.postCall(httpClient, builder.build().toURL().toString(), headers, jsonData); + Map envelop; + // to unify the same behaviour then V2, this method handle Offline Exception and return a dummy shot with + // offline flag. + try { + envelop = handleGenericErrorResult(resp); + } catch (LGThinqDeviceV1OfflineException e) { + return buildDefaultOfflineSnapshot(); + } + Map workList = objectMapper + .convertValue(envelop.getOrDefault("workList", Collections.emptyMap()), new TypeReference<>() { + }); + if (workList.get("returnData") != null) { + if (logger.isDebugEnabled()) { + try { + objectMapper.writeValue(new File(String.format( + LGThinQBindingConstants.THINQ_USER_DATA_FOLDER + File.separator + "thinq-%s-datatrace.json", + deviceId)), workList); + } catch (IOException e) { + // Only debug since datatrace is a trace data. + logger.debug("Unexpected error saving data trace", e); + } + } + + if (!ResultCodes.OK.containsResultCode("" + workList.get("returnCode"))) { + String code = Objects.requireNonNullElse((String) workList.get("returnCode"), ""); + logger.debug("LG API report error processing the request -> resultCode=[{}], message=[{}]", code, + getErrorCodeMessage(code)); + LGThinqDeviceV1MonitorExpiredException e = new LGThinqDeviceV1MonitorExpiredException( + String.format("Monitor for device %s has expired. Please, refresh the monitor.", deviceId)); + logger.warn("{}", e.getMessage()); + throw e; + } + + String monDataB64 = (String) workList.get("returnData"); + String monData = new String(Base64.getDecoder().decode(monDataB64)); + S shot; + try { + if (MonitoringResultFormat.JSON_FORMAT.equals(deviceCapability.getMonitoringDataFormat())) { + shot = (S) SnapshotBuilderFactory.getInstance().getBuilder(snapshotClass).createFromJson(monData, + deviceType, deviceCapability); + } else if (MonitoringResultFormat.BINARY_FORMAT.equals(deviceCapability.getMonitoringDataFormat())) { + shot = (S) SnapshotBuilderFactory.getInstance().getBuilder(snapshotClass).createFromBinary(monData, + deviceCapability.getMonitoringBinaryProtocol(), deviceCapability); + } else { + throw new LGThinqApiException(String.format("Returned data format not supported: %s", + deviceCapability.getMonitoringDataFormat())); + } + shot.setOnline("E".equals(workList.get("deviceState"))); + } catch (LGThinqUnmarshallException ex) { + // error in the monitor Data returned. Device is irresponsible + logger.debug("Monitored data returned for the device {} is unreadable. Device is not connected", + deviceId); + throw ex; + } + return shot; + } else { + // no data available yet + return null; + } + } + + @Override + public void initializeDevice(String bridgeName, String deviceId) throws LGThinqApiException { + logger.debug("Initializing device [{}] from bridge [{}]", deviceId, bridgeName); + } + + /** + * Perform some routine before getting data device. Depending on the kind of the device, this is needed + * to update or prepare some informations before go to get the data. + * + * @return false if the device doesn't support pre-condition commands + */ + protected abstract boolean beforeGetDataDevice(String bridgeName, String deviceId); + + /** + * Get snapshot data from the device. + * It works only for API V2 device versions! + * + * @param deviceId device ID for de desired V2 LG Thinq. + * @param capDef + * @return return map containing metamodel of settings and snapshot + * @throws LGThinqApiException if some communication error occur. + */ + @Override + @Nullable + public S getDeviceData(String bridgeName, String deviceId, CapabilityDefinition capDef) throws LGThinqApiException { + // Exec pre-conditions (normally ask for update monitoring sensors of the device - temp and power) before call + // for data + if (capDef.isBeforeCommandSupported() && !beforeGetDataDevice(bridgeName, deviceId)) { + capDef.setBeforeCommandSupported(false); + } + + Map deviceSettings = getDeviceSettings(bridgeName, deviceId); + if (deviceSettings.get("snapshot") != null) { + Map snapMap = (Map) deviceSettings.get("snapshot"); + if (logger.isDebugEnabled()) { + try { + objectMapper.writeValue(new File(String.format( + LGThinQBindingConstants.THINQ_USER_DATA_FOLDER + File.separator + "thinq-%s-datatrace.json", + deviceId)), deviceSettings); + } catch (IOException e) { + logger.debug("Error saving data trace", e); + } + } + if (snapMap == null) { + // No snapshot value provided + return null; + } + S shot = (S) SnapshotBuilderFactory.getInstance().getBuilder(snapshotClass).createFromJson(deviceSettings, + capDef); + shot.setOnline((Boolean) snapMap.getOrDefault("online", Boolean.FALSE)); + return shot; + } + return null; + } + + /** + * Start monitor data form specific device. This is old one, works only on V1 API supported devices. + * + * @param deviceId Device ID + * @return Work1 to be uses to grab data during monitoring. + * @throws LGThinqApiException If some communication error occur. + */ + @Override + public String startMonitor(String bridgeName, String deviceId) throws LGThinqApiException, IOException { + TokenResult token = tokenManager.getValidRegisteredToken(bridgeName); + UriBuilder builder = UriBuilder.fromUri(token.getGatewayInfo().getApiRootV1()).path(LG_API_V1_START_MON_PATH); + Map headers = getCommonHeaders(token.getGatewayInfo().getLanguage(), + token.getGatewayInfo().getCountry(), token.getAccessToken(), token.getUserInfo().getUserNumber()); + String workerId = UUID.randomUUID().toString(); + String jsonData = String.format(" { \"lgedmRoot\" : {" + "\"cmd\": \"Mon\"," + "\"cmdOpt\": \"Start\"," + + "\"deviceId\": \"%s\"," + "\"workId\": \"%s\"" + "} }", deviceId, workerId); + RestResult resp = RestUtils.postCall(httpClient, builder.build().toURL().toString(), headers, jsonData); + Map respMap = handleGenericErrorResult(resp); + if (respMap.isEmpty()) { + logger.debug( + "Unexpected StartMonitor json null result. Possible causes: 1) you are monitoring the device in LG App at same time, 2) temporary problems in the server. Try again later"); + } + return Objects.requireNonNull((String) handleGenericErrorResult(resp).get("workId"), + "Unexpected StartMonitor json result. Node 'workId' not present"); + } + + @Override + public void stopMonitor(String bridgeName, String deviceId, String workId) throws LGThinqApiException, IOException { + TokenResult token = tokenManager.getValidRegisteredToken(bridgeName); + UriBuilder builder = UriBuilder.fromUri(token.getGatewayInfo().getApiRootV1()).path(LG_API_V1_START_MON_PATH); + Map headers = getCommonHeaders(token.getGatewayInfo().getLanguage(), + token.getGatewayInfo().getCountry(), token.getAccessToken(), token.getUserInfo().getUserNumber()); + String jsonData = String.format(" { \"lgedmRoot\" : {" + "\"cmd\": \"Mon\"," + "\"cmdOpt\": \"Stop\"," + + "\"deviceId\": \"%s\"," + "\"workId\": \"%s\"" + "} }", deviceId, workId); + RestResult resp = RestUtils.postCall(httpClient, builder.build().toURL().toString(), headers, jsonData); + handleGenericErrorResult(resp); + } + + protected Map getCommonV2Headers(String language, String country, String accessToken, + String userNumber) { + return getCommonHeaders(language, country, accessToken, userNumber); + } + + protected abstract RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, String keyName, String value) throws Exception; + + protected abstract RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, @Nullable String keyName, @Nullable String value, @Nullable ObjectNode extraNode) + throws Exception; + + protected abstract Map handleGenericErrorResult(@Nullable RestResult resp) + throws LGThinqApiException; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiV1ClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiV1ClientService.java new file mode 100644 index 0000000000000..125c67e658849 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiV1ClientService.java @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V1_CONTROL_OP; + +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.api.RestUtils; +import org.openhab.binding.lgthinq.lgservices.api.TokenResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqDeviceV1OfflineException; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.ResultCodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * The {@link LGThinQAbstractApiV1ClientService} - Specialized abstract class that implements methods and services to + * handle LG API V1 communication and convention. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class LGThinQAbstractApiV1ClientService + extends LGThinQAbstractApiClientService { + private static final Logger logger = LoggerFactory.getLogger(LGThinQAbstractApiV1ClientService.class); + + protected LGThinQAbstractApiV1ClientService(Class capabilityClass, Class snapshotClass, + HttpClient httpClient) { + super(capabilityClass, snapshotClass, httpClient); + } + + @Override + protected RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, String keyName, String value) throws Exception { + return sendCommand(bridgeName, deviceId, controlPath, controlKey, command, keyName, value, null); + } + + protected RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, Map<@Nullable String, @Nullable Object> keyValue, @Nullable ObjectNode extraNode) + throws Exception { + ObjectNode payloadNode = JsonNodeFactory.instance.objectNode(); + payloadNode.put("cmd", controlKey).put("cmdOpt", command); + keyValue.forEach((k, v) -> { + if (k == null || k.isEmpty()) { + // value is a simple text + if (v instanceof Integer i) { + payloadNode.put("value", i); + } else if (v instanceof Double d) { + payloadNode.put("value", d); + } else { + payloadNode.put("value", "" + v); + } + } else { + JsonNode valueNode = payloadNode.path("value"); + if (valueNode.isMissingNode()) { + valueNode = payloadNode.putObject("value"); + } + if (v instanceof Integer i) { + ((ObjectNode) valueNode).put(k, i); + } else if (v instanceof Double d) { + ((ObjectNode) valueNode).put(k, d); + } else { + ((ObjectNode) valueNode).put(k, "" + v); + } + } + }); + if (extraNode != null) { + payloadNode.setAll(extraNode); + } + return sendCommand(bridgeName, deviceId, payloadNode); + } + + protected RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, @Nullable String keyName, @Nullable String value, @Nullable ObjectNode extraNode) + throws Exception { + Map<@Nullable String, @Nullable Object> values = new HashMap<>(1); + values.put(keyName, value); + return sendCommand(bridgeName, deviceId, controlPath, controlKey, command, values, extraNode); + } + + protected RestResult sendCommand(String bridgeName, String deviceId, Object cmdPayload) throws Exception { + TokenResult token = tokenManager.getValidRegisteredToken(bridgeName); + UriBuilder builder = UriBuilder.fromUri(token.getGatewayInfo().getApiRootV1()).path(LG_API_V1_CONTROL_OP); + Map headers = getCommonHeaders(token.getGatewayInfo().getLanguage(), + token.getGatewayInfo().getCountry(), token.getAccessToken(), token.getUserInfo().getUserNumber()); + ObjectNode payloadNode; + if (cmdPayload instanceof ObjectNode) { + payloadNode = ((ObjectNode) cmdPayload).deepCopy(); + } else { + payloadNode = objectMapper.convertValue(cmdPayload, new TypeReference<>() { + }); + } + ObjectNode rootNode = JsonNodeFactory.instance.objectNode(); + ObjectNode bodyNode = JsonNodeFactory.instance.objectNode(); + bodyNode.put("deviceId", deviceId); + bodyNode.put("workId", UUID.randomUUID().toString()); + bodyNode.setAll(payloadNode); + rootNode.set("lgedmRoot", bodyNode); + String url = builder.build().toURL().toString(); + logger.debug("URL: {}, Post Payload:[{}]", url, rootNode.toPrettyString()); + RestResult resp = RestUtils.postCall(httpClient, url, headers, rootNode.toPrettyString()); + if (resp == null) { + logger.warn("Null result returned sending command to LG API V1"); + throw new LGThinqApiException("Null result returned sending command to LG API V1"); + } + return resp; + } + + @Override + protected Map handleGenericErrorResult(@Nullable RestResult resp) throws LGThinqApiException { + Map metaResult; + Map envelope = Collections.emptyMap(); + if (resp == null) { + return envelope; + } + if (resp.getStatusCode() != 200) { + if (resp.getStatusCode() == 400) { + logger.warn("Error returned by LG Server API. HTTP Status: {}. The reason is: {}", resp.getStatusCode(), + resp.getJsonResponse()); + } else { + throw new LGThinqApiException( + String.format("Error returned by LG Server API. HTTP Status: %s. The reason is: %s", + resp.getStatusCode(), resp.getJsonResponse())); + } + } else { + try { + metaResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + envelope = objectMapper.convertValue(metaResult.get("lgedmRoot"), new TypeReference<>() { + }); + String code = String.valueOf(envelope.get("returnCd")); + if (envelope.isEmpty()) { + throw new LGThinqApiException(String.format( + "Unexpected json body returned (without root node lgedmRoot): %s", resp.getJsonResponse())); + } else if (!ResultCodes.OK.containsResultCode(code)) { + if (ResultCodes.DEVICE_NOT_RESPONSE.containsResultCode("" + envelope.get("returnCd")) + || "D".equals(envelope.get("deviceState"))) { + logger.debug("LG API report error processing the request -> resultCode=[{}], message=[{}]", + code, getErrorCodeMessage(code)); + // Disconnected Device + throw new LGThinqDeviceV1OfflineException("Device is offline. No data available"); + } + throw new LGThinqApiException(String + .format("Status error executing endpoint. resultCode must be 0000, but was:%s", code)); + } + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unknown error occurred deserializing json stream", e); + } + } + return envelope; + } + + /** + * Principal method to prepare the command to be sent to V1 Devices mainly when the command is generic, + * i.e, you can send a command structure to redefine any changeable feature of the device + * + * @param cmdDef command definition with template of the payload and data (binary or not) + * @param snapData snapshot data with features to be set in the device + * @return return the command structure. + * @throws JsonProcessingException - unmarshall error. + */ + protected Map prepareCommandV1(CommandDefinition cmdDef, Map snapData) + throws JsonProcessingException { + // expected map ordered here + String dataStr = cmdDef.getDataTemplate(); + // Keep the order + for (Map.Entry e : snapData.entrySet()) { + String value = String.valueOf(e.getValue()); + dataStr = dataStr.replace("{{" + e.getKey() + "}}", value); + } + + return completeCommandDataNodeV1(cmdDef, dataStr); + } + + protected LinkedHashMap completeCommandDataNodeV1(CommandDefinition cmdDef, String dataStr) + throws JsonProcessingException { + LinkedHashMap data = objectMapper.readValue(cmdDef.getRawCommand(), new TypeReference<>() { + }); + logger.debug("Prepare command v1: {}", dataStr); + if (cmdDef.isBinary()) { + data.put("format", "B64"); + List list = objectMapper.readValue(dataStr, new TypeReference<>() { + }); + // convert the list of integer to a bytearray + + byte[] byteArray = new byte[list.size()]; + for (int i = 0; i < list.size(); i++) { + byteArray[i] = list.get(i).byteValue(); // Converte Integer para byte + } + String str_data_encoded = new String(Base64.getEncoder().encode(byteArray)); + data.put("data", str_data_encoded); + } else { + data.put("data", dataStr); + } + return data; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiV2ClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiV2ClientService.java new file mode 100644 index 0000000000000..6634d4fd00121 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiV2ClientService.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_CTRL_DEVICE_CONFIG_PATH; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.api.RestUtils; +import org.openhab.binding.lgthinq.lgservices.api.TokenResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.ResultCodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * The {@link LGThinQAbstractApiV2ClientService} - Specialized abstract class that implements methods and services to + * * handle LG API V2 communication and convention. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class LGThinQAbstractApiV2ClientService + extends LGThinQAbstractApiClientService { + private static final Logger logger = LoggerFactory.getLogger(LGThinQAbstractApiV2ClientService.class); + + protected LGThinQAbstractApiV2ClientService(Class capabilityClass, Class snapshotClass, + HttpClient httpClient) { + super(capabilityClass, snapshotClass, httpClient); + } + + @Override + protected RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, String keyName, String value) throws Exception { + return sendCommand(bridgeName, deviceId, controlPath, controlKey, command, keyName, value, null); + } + + protected RestResult postCall(String bridgeName, String deviceId, String controlPath, String payload) + throws LGThinqApiException, IOException { + TokenResult token = tokenManager.getValidRegisteredToken(bridgeName); + UriBuilder builder = UriBuilder.fromUri(token.getGatewayInfo().getApiRootV2()) + .path(String.format(LG_API_V2_CTRL_DEVICE_CONFIG_PATH, deviceId, controlPath)); + Map headers = getCommonV2Headers(token.getGatewayInfo().getLanguage(), + token.getGatewayInfo().getCountry(), token.getAccessToken(), token.getUserInfo().getUserNumber()); + RestResult resp = RestUtils.postCall(httpClient, builder.build().toURL().toString(), headers, payload); + if (resp == null) { + logger.warn("Null result returned sending command to LG API V2: {}, {}, {}", deviceId, controlPath, + payload); + throw new LGThinqApiException("Null result returned sending command to LG API V2"); + } + return resp; + } + + @Override + public RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, @Nullable String keyName, @Nullable String value, @Nullable ObjectNode extraNode) + throws Exception { + ObjectNode payload = JsonNodeFactory.instance.objectNode(); + payload.put("ctrlKey", controlKey).put("command", command).put("dataKey", keyName).put("dataValue", value); + if (extraNode != null) { + payload.setAll(extraNode); + } + return postCall(bridgeName, deviceId, controlPath, payload.toPrettyString()); + } + + protected RestResult sendBasicControlCommands(String bridgeName, String deviceId, String command, String keyName, + int value) throws Exception { + return sendCommand(bridgeName, deviceId, "control-sync", "basicCtrl", command, keyName, String.valueOf(value)); + } + + @Override + protected Map handleGenericErrorResult(@Nullable RestResult resp) throws LGThinqApiException { + Map metaResult; + if (resp == null) { + return Collections.emptyMap(); + } + if (resp.getStatusCode() != 200) { + if (resp.getStatusCode() == 400) { + if (logger.isDebugEnabled()) { + logger.warn("Error returned by LG Server API. HTTP Status: {}. The reason is: {}\n {}", + resp.getStatusCode(), resp.getJsonResponse(), Thread.currentThread().getStackTrace()); + } else { + logger.warn("Error returned by LG Server API. HTTP Status: {}. The reason is: {}", + resp.getStatusCode(), resp.getJsonResponse()); + } + return Collections.emptyMap(); + } else { + throw new LGThinqApiException( + String.format("Error returned by LG Server API. HTTP Status: %s. The reason is: %s", + resp.getStatusCode(), resp.getJsonResponse())); + } + } else { + try { + metaResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + String code = (String) metaResult.get("resultCode"); + if (!ResultCodes.OK.containsResultCode(String.valueOf(metaResult.get("resultCode")))) { + throw new LGThinqApiException( + String.format("LG API report error processing the request -> resultCode=[%s], message=[%s]", + code, getErrorCodeMessage(code))); + } + return metaResult; + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unknown error occurred deserializing json stream", e); + } + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQApiClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQApiClientService.java new file mode 100644 index 0000000000000..c834ff69e33f3 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQApiClientService.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqDeviceV1MonitorExpiredException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.LGDevice; +import org.openhab.binding.lgthinq.lgservices.model.SnapshotDefinition; + +/** + * The {@link LGThinQApiClientService} - defines the basic methods to manage devices in the LG Cloud + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface LGThinQApiClientService { + /** + * List all devices registers in the LG Account + * + * @param bridgeName bridge name + * @return return a List off all devices registered for the user account. + * @throws LGThinqApiException if some error occur accessing LG API + */ + List listAccountDevices(String bridgeName) throws LGThinqApiException; + + /** + * Get the LG device metadata about the settings and capabilities of the Device + * + * @param bridgeName bridge name + * @param deviceId LG Device ID + * @return A map containing all the device settings. + * @throws LGThinqApiException + */ + Map getDeviceSettings(String bridgeName, String deviceId) throws LGThinqApiException; + + void initializeDevice(String bridgeName, String deviceId) throws LGThinqApiException; + + /** + * Retrieve actual data from device (its sensors and points states). + * + * @param deviceId device number + * @param capDef Capabilities definition/settings of the device + * @return return snapshot state of the device sensors and features + * @throws LGThinqApiException if some error interacting with LG API Server occur. + */ + @Nullable + S getDeviceData(String bridgeName, String deviceId, CapabilityDefinition capDef) throws LGThinqApiException; + + /** + * Turn on/off the device + * + * @param bridgeName bridge name + * @param deviceId LG device ID + * @param newPowerState new Power State + * @throws LGThinqApiException if some error interacting with LG API Server occur. + */ + void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) throws LGThinqApiException; + + /** + * Start the device Monitor responsible to open a window of data collection. (only used for V1 protocol) + * + * @param bridgeName bridge name + * @param deviceId LG device ID + * @return string with the monitor ID + * @throws LGThinqApiException if some error interacting with LG API Server occur. + * @throws IOException if some error occur opening device's configuration files. + */ + String startMonitor(String bridgeName, String deviceId) throws LGThinqApiException, IOException; + + /** + * Get the capabilities of the device (got from device settings) + * + * @param deviceId The LG device ID + * @param uri the URL containing the XML descriptor of the device + * @param forceRecreate If you want to recreate the cached file of the XML descriptor + * @return the capability object related to the device + * @throws LGThinqApiException if some error interacting with LG API Server occur. + */ + C getCapability(String deviceId, String uri, boolean forceRecreate) throws LGThinqApiException; + + /** + * Build a default snapshot data of the device when it's offline, junto to keep data integrity in the channels + * + * @return Default snapshot. + */ + S buildDefaultOfflineSnapshot(); + + /** + * Load device capabilities from the cached file. + * + * @param deviceId LG Thinq Device ID + * @param uri if the file doesn't exist, get the content from registered URI and save locally. + * @param forceRecreate force to recreate the file even if was previously saved locally + * @return File pointing to the capability file + * @throws LGThinqApiException if some error interacting with LG API Server occur. + */ + File loadDeviceCapability(String deviceId, String uri, boolean forceRecreate) throws LGThinqApiException; + + /** + * Stop the monitor of data collection + * + * @param bridgeName Bridge name + * @param deviceId LG Device ID + * @param workId name of the monitor + * @throws LGThinqApiException if some error interacting with LG API Server occur. + * @throws IOException if some error occur opening device's configuration files. + */ + void stopMonitor(String bridgeName, String deviceId, String workId) throws LGThinqException, IOException; + + /** + * Get data collected by the monitor + * + * @param bridgeName Bridge name + * @param deviceId LG Device ID + * @param workerId monitor ID + * @param deviceType Device Type related to the data collected + * @param deviceCapability capabilities of the device + * @return Snapshot of the device collected from LG API + * @throws LGThinqApiException if some error is returned from LG API + * @throws LGThinqDeviceV1MonitorExpiredException if the monitor is not valid anymore + * @throws IOException if some IO error happen when accessing token cache file. + * @throws LGThinqUnmarshallException if some error happen reading data collected from LG API + */ + @Nullable + S getMonitorData(String bridgeName, String deviceId, String workerId, DeviceTypes deviceType, C deviceCapability) + throws LGThinqApiException, LGThinqDeviceV1MonitorExpiredException, IOException, LGThinqUnmarshallException; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQApiClientServiceFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQApiClientServiceFactory.java new file mode 100644 index 0000000000000..b18827db26e8a --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQApiClientServiceFactory.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_PLATFORM_TYPE_V1; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapability; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.core.io.net.http.HttpClientFactory; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Creates specialized API clients. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQApiClientServiceFactory { + + public static LGThinQGeneralApiClientService newGeneralApiClientService(HttpClientFactory httpClientFactory) { + return new LGThinQGeneralApiClientService(httpClientFactory.getCommonHttpClient()); + } + + public static LGThinQACApiClientService newACApiClientService(String lgPlatformType, + HttpClientFactory httpClientFactory) { + return lgPlatformType.equals(LG_API_PLATFORM_TYPE_V1) + ? new LGThinQACApiV1ClientServiceImpl(httpClientFactory.getCommonHttpClient()) + : new LGThinQACApiV2ClientServiceImpl(httpClientFactory.getCommonHttpClient()); + } + + public static LGThinQFridgeApiClientService newFridgeApiClientService(String lgPlatformType, + HttpClientFactory httpClientFactory) { + return lgPlatformType.equals(LG_API_PLATFORM_TYPE_V1) + ? new LGThinQFridgeApiV1ClientServiceImpl(httpClientFactory.getCommonHttpClient()) + : new LGThinQFridgeApiV2ClientServiceImpl(httpClientFactory.getCommonHttpClient()); + } + + public static LGThinQWMApiClientService newWMApiClientService(String lgPlatformType, + HttpClientFactory httpClientFactory) { + return lgPlatformType.equals(LG_API_PLATFORM_TYPE_V1) + ? new LGThinQWMApiV1ClientServiceImpl(httpClientFactory.getCommonHttpClient()) + : new LGThinQWMApiV2ClientServiceImpl(httpClientFactory.getCommonHttpClient()); + } + + public static LGThinQDishWasherApiClientService newDishWasherApiClientService(String lgPlatformType, + HttpClientFactory httpClientFactory) { + return lgPlatformType.equals(LG_API_PLATFORM_TYPE_V1) + ? new LGThinQDishWasherApiV1ClientServiceImpl(httpClientFactory.getCommonHttpClient()) + : new LGThinQDishWasherApiV2ClientServiceImpl(httpClientFactory.getCommonHttpClient()); + } + + public static final class LGThinQGeneralApiClientService + extends LGThinQAbstractApiClientService { + + private LGThinQGeneralApiClientService(HttpClient httpClient) { + super(GenericCapability.class, AbstractSnapshotDefinition.class, httpClient); + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + throw new UnsupportedOperationException(); + } + + @Override + protected RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, String keyName, String value) { + throw new UnsupportedOperationException(); + } + + @Override + protected RestResult sendCommand(String bridgeName, String deviceId, String controlPath, String controlKey, + String command, @Nullable String keyName, @Nullable String value, @Nullable ObjectNode extraNode) { + throw new UnsupportedOperationException(); + } + + @Override + protected Map handleGenericErrorResult(@Nullable RestResult resp) { + throw new UnsupportedOperationException(); + } + } + + private static final class GenericCapability extends AbstractCapability { + + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiClientService.java new file mode 100644 index 0000000000000..ecbbccc7c0fbb --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiClientService.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherSnapshot; + +/** + * The {@link LGThinQDishWasherApiClientService} - implements specific methods for DishWashers + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface LGThinQDishWasherApiClientService + extends LGThinQApiClientService { + /** + * Remote start machine funcion + * + * @param bridgeName Bridge name + * @param cap Capabilities of the device + * @param deviceId LG Device ID + * @param data data to sent to remote start + */ + void remoteStart(String bridgeName, DishWasherCapability cap, String deviceId, Map data); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiV1ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiV1ClientServiceImpl.java new file mode 100644 index 0000000000000..791a0a2a5b7e4 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiV1ClientServiceImpl.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherSnapshot; + +/** + * The {@link LGThinQDishWasherApiV1ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQDishWasherApiV1ClientServiceImpl + extends LGThinQAbstractApiV1ClientService + implements LGThinQDishWasherApiClientService { + + protected LGThinQDishWasherApiV1ClientServiceImpl(HttpClient httpClient) { + super(DishWasherCapability.class, DishWasherSnapshot.class, httpClient); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + // there's no before settings to send command + return false; + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) { + throw new UnsupportedOperationException("Not Supported for this device"); + } + + @Override + @Nullable + public DishWasherSnapshot getDeviceData(String bridgeName, String deviceId, CapabilityDefinition capDef) { + throw new UnsupportedOperationException("Method not supported in V1 API device."); + } + + @Override + public void remoteStart(String bridgeName, DishWasherCapability cap, String deviceId, Map data) { + throw new UnsupportedOperationException("Not implemented yet"); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiV2ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiV2ClientServiceImpl.java new file mode 100644 index 0000000000000..238f6d2b7c838 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQDishWasherApiV2ClientServiceImpl.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherSnapshot; + +/** + * The {@link LGThinQDishWasherApiV2ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQDishWasherApiV2ClientServiceImpl + extends LGThinQAbstractApiV2ClientService + implements LGThinQDishWasherApiClientService { + + protected LGThinQDishWasherApiV2ClientServiceImpl(HttpClient httpClient) { + super(DishWasherCapability.class, DishWasherSnapshot.class, httpClient); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + // there's no before settings to send command + return false; + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) { + throw new UnsupportedOperationException("Unsupported for this device"); + } + + @Override + public void remoteStart(String bridgeName, DishWasherCapability cap, String deviceId, Map data) { + throw new UnsupportedOperationException("Not implemented yet"); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiClientService.java new file mode 100644 index 0000000000000..18f826538ec98 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiClientService.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCapability; + +/** + * The {@link LGThinQFridgeApiClientService} - Interface with specific methods for Fridge Devices + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface LGThinQFridgeApiClientService + extends LGThinQApiClientService { + /** + * Set fridge temperature + * + * @param bridgeId Bridge ID + * @param deviceId LG Device ID + * @param fridgeCapability Fridge Capabilities + * @param targetTemperatureIndex target temperature + * @param tempUnit Temperature Unit + * @param snapCmdData Snapshot template for the target temperature command + * @throws LGThinqApiException If some error is reported from LG API + */ + void setFridgeTemperature(String bridgeId, String deviceId, FridgeCapability fridgeCapability, + Integer targetTemperatureIndex, String tempUnit, @Nullable Map snapCmdData) + throws LGThinqApiException; + + /** + * Set fridge temperature + * + * @param bridgeId Bridge ID + * @param deviceId LG Device ID + * @param fridgeCapability Fridge Capabilities + * @param targetTemperatureIndex target temperature + * @param tempUnit Temperature Unit + * @param snapCmdData Snapshot template for the target temperature command + * @throws LGThinqApiException If some error is reported from LG API + */ + void setFreezerTemperature(String bridgeId, String deviceId, FridgeCapability fridgeCapability, + Integer targetTemperatureIndex, String tempUnit, @Nullable Map snapCmdData) + throws LGThinqApiException; + + /** + * Setup Express Mode + * + * @param bridgeId Bridge ID + * @param deviceId LG Device ID + * @param expressModeIndex Empress mode desired + * @throws LGThinqApiException If some error is reported from LG API + */ + void setExpressMode(String bridgeId, String deviceId, String expressModeIndex) throws LGThinqApiException; + + /** + * Set the Express Cool Mode + * + * @param bridgeId Bridge ID + * @param deviceId LG Device id + * @param trueOnFalseOff ON/OFF the Cool Mode + * @throws LGThinqApiException If some error is reported from LG API + */ + void setExpressCoolMode(String bridgeId, String deviceId, boolean trueOnFalseOff) throws LGThinqApiException; + + /** + * Set the Express Cool Mode + * + * @param bridgeId Bridge ID + * @param deviceId LG Device id + * @param trueOnFalseOff ON/OFF the Eco Mode + * @throws LGThinqApiException If some error is reported from LG API + */ + void setEcoFriendlyMode(String bridgeId, String deviceId, boolean trueOnFalseOff) throws LGThinqApiException; + + /** + * + * @param bridgeId Bridge ID + * @param deviceId LG Thinq Device ID + * @param fridgeCapability Fridge Capabilities + * @param trueOnFalseOff Set ON/OFF the ICE Plus + * @param snapCmdData Snapshot template for the ICE Plus Command + * @throws LGThinqApiException If some error is reported from LG API + */ + void setIcePlus(String bridgeId, String deviceId, FridgeCapability fridgeCapability, boolean trueOnFalseOff, + Map snapCmdData) throws LGThinqApiException; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiV1ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiV1ClientServiceImpl.java new file mode 100644 index 0000000000000..74f1b85d6a685 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiV1ClientServiceImpl.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_SET_CONTROL_COMMAND_NAME_V1; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCapability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LGThinQFridgeApiV1ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQFridgeApiV1ClientServiceImpl + extends LGThinQAbstractApiV1ClientService + implements LGThinQFridgeApiClientService { + private static final Logger logger = LoggerFactory.getLogger(LGThinQFridgeApiV1ClientServiceImpl.class); + + protected LGThinQFridgeApiV1ClientServiceImpl(HttpClient httpClient) { + super(FridgeCapability.class, FridgeCanonicalSnapshot.class, httpClient); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + // there's no before settings to send command + return false; + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + @Nullable + public FridgeCanonicalSnapshot getDeviceData(String bridgeName, String deviceId, CapabilityDefinition capDef) { + throw new UnsupportedOperationException("Method not supported in V1 API device."); + } + + @Override + public void setFridgeTemperature(String bridgeId, String deviceId, FridgeCapability fridgeCapability, + Integer targetTemperatureIndex, String tempUnit, @Nullable Map snapCmdData) + throws LGThinqApiException { + if (snapCmdData != null) { + snapCmdData.put("TempRefrigerator", targetTemperatureIndex); + setControlCommand(bridgeId, deviceId, fridgeCapability, snapCmdData); + } else { + logger.warn("Snapshot Command Data is null"); + } + } + + @Override + public void setFreezerTemperature(String bridgeId, String deviceId, FridgeCapability fridgeCapability, + Integer targetTemperatureIndex, String tempUnit, @Nullable Map snapCmdData) + throws LGThinqApiException { + if (snapCmdData != null) { + snapCmdData.put("TempFreezer", targetTemperatureIndex); + setControlCommand(bridgeId, deviceId, fridgeCapability, snapCmdData); + } else { + logger.warn("Snapshot command is null"); + } + } + + @Override + public void setExpressMode(String bridgeId, String deviceId, String expressModeIndex) { + throw new UnsupportedOperationException("V1 Fridge doesn't support ExpressMode feature. It mostly like a bug"); + } + + @Override + public void setExpressCoolMode(String bridgeId, String deviceId, boolean trueOnFalseOff) { + throw new UnsupportedOperationException( + "V1 Fridge doesn't support ExpressCoolMode feature. It mostly like a bug"); + } + + @Override + public void setEcoFriendlyMode(String bridgeId, String deviceId, boolean trueOnFalseOff) { + throw new UnsupportedOperationException( + "V1 Fridge doesn't support ExpressCoolMode feature. It mostly like a bug"); + } + + @Override + public void setIcePlus(String bridgeId, String deviceId, FridgeCapability fridgeCapability, boolean trueOnFalseOff, + Map snapCmdData) throws LGThinqApiException { + snapCmdData.put("IcePlus", trueOnFalseOff ? 1 : 0); + setControlCommand(bridgeId, deviceId, fridgeCapability, snapCmdData); + } + + private void setControlCommand(String bridgeId, String deviceId, FridgeCapability fridgeCapability, + @Nullable Map snapCmdData) throws LGThinqApiException { + try { + CommandDefinition cmdSetControlDef = fridgeCapability.getCommandsDefinition() + .get(RE_SET_CONTROL_COMMAND_NAME_V1); + if (cmdSetControlDef == null) { + logger.warn("No command definition found for set control command. Ignoring command"); + return; + } + if (snapCmdData == null) { + logger.error("Snapshot to complete command was not send. It's mostly like a bug"); + return; + } + Map cmdPayload = prepareCommandV1(cmdSetControlDef, snapCmdData); + logger.debug("setControl Payload:[{}]", cmdPayload); + RestResult result = sendCommand(bridgeId, deviceId, cmdPayload); + handleGenericErrorResult(result); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error sending remote start", e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiV2ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiV2ClientServiceImpl.java new file mode 100644 index 0000000000000..d75828eaa9ef9 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQFridgeApiV2ClientServiceImpl.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCapability; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * The {@link LGThinQFridgeApiV2ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQFridgeApiV2ClientServiceImpl + extends LGThinQAbstractApiV2ClientService + implements LGThinQFridgeApiClientService { + + protected LGThinQFridgeApiV2ClientServiceImpl(HttpClient httpClient) { + super(FridgeCapability.class, FridgeCanonicalSnapshot.class, httpClient); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + // there's no before settings to send command + return false; + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + public void setFridgeTemperature(String bridgeId, String deviceId, FridgeCapability fridgeCapability, + Integer targetTemperatureIndex, String tempUnit, @Nullable Map snapCmdData) + throws LGThinqApiException { + setTemperature("fridgeTemp", bridgeId, deviceId, targetTemperatureIndex, tempUnit); + } + + @Override + public void setFreezerTemperature(String bridgeId, String deviceId, FridgeCapability fridgeCapability, + Integer targetTemperatureIndex, String tempUnit, @Nullable Map snapCmdData) + throws LGThinqApiException { + setTemperature("freezerTemp", bridgeId, deviceId, targetTemperatureIndex, tempUnit); + } + + @Override + public void setExpressMode(String bridgeId, String deviceId, String expressMode) throws LGThinqApiException { + sendSimpleDataSetListCommand(bridgeId, deviceId, "expressMode", expressMode); + } + + private void sendSimpleDataSetListCommand(String bridgeId, String deviceId, String feature, String value) + throws LGThinqApiException { + ObjectNode dataSetList = JsonNodeFactory.instance.objectNode(); + ObjectNode nodeData = dataSetList.putObject("dataSetList").putObject("refState"); + nodeData.put(feature, value); + try { + RestResult result = sendCommand(bridgeId, deviceId, "control-sync", "basicCtrl", "Set", null, null, + dataSetList); + handleGenericErrorResult(result); + } catch (Exception e) { + throw new LGThinqApiException("Error sending command", e); + } + } + + @Override + public void setExpressCoolMode(String bridgeId, String deviceId, boolean trueOnFalseOff) + throws LGThinqApiException { + sendSimpleDataSetListCommand(bridgeId, deviceId, "expressFridge", trueOnFalseOff ? "ON" : "OFF"); + } + + @Override + public void setEcoFriendlyMode(String bridgeId, String deviceId, boolean trueOnFalseOff) + throws LGThinqApiException { + sendSimpleDataSetListCommand(bridgeId, deviceId, "ecoFriendly", trueOnFalseOff ? "ON" : "OFF"); + } + + @Override + public void setIcePlus(String bridgeId, String deviceId, FridgeCapability fridgeCapability, boolean trueOnFalseOff, + Map snapCmdData) { + throw new UnsupportedOperationException("V2 Fridge doesn't support IcePlus feature. It mostly like a bug"); + } + + private void setTemperature(String tempFeature, String bridgeId, String deviceId, Integer targetTemperature, + String tempUnit) throws LGThinqApiException { + ObjectNode dataSetList = JsonNodeFactory.instance.objectNode(); + ObjectNode nodeData = dataSetList.putObject("dataSetList").putObject("refState"); + nodeData.put(tempFeature, targetTemperature).put("tempUnit", tempUnit); + try { + RestResult result = sendCommand(bridgeId, deviceId, "control-sync", "basicCtrl", "Set", null, null, + dataSetList); + handleGenericErrorResult(result); + } catch (Exception e) { + throw new LGThinqApiException("Error sending command", e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiClientService.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiClientService.java new file mode 100644 index 0000000000000..02c32aed25a55 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiClientService.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerSnapshot; + +/** + * The {@link LGThinQWMApiClientService} - Methods specifics for Washing/Drier Machines + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface LGThinQWMApiClientService extends LGThinQApiClientService { + /** + * Control the remote start feature + * + * @param bridgeName Bridge Name + * @param cap Capabilities of the device + * @param deviceId LG Device ID + * @param data Data to control the remote start + * @throws LGThinqApiException if some error is reported from the LG API + */ + void remoteStart(String bridgeName, WasherDryerCapability cap, String deviceId, Map data) + throws LGThinqApiException; + + /** + * Waking UP feature + * + * @param bridgeName Bridge Name + * @param deviceId LG Device Name + * @param wakeUp to Wake Up (true/false) + * @throws LGThinqApiException if some error is reported from the LG API + */ + void wakeUp(String bridgeName, String deviceId, Boolean wakeUp) throws LGThinqApiException; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiV1ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiV1ClientServiceImpl.java new file mode 100644 index 0000000000000..ca49c2a1e1b71 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiV1ClientServiceImpl.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerSnapshot; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * The {@link LGThinQWMApiV1ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQWMApiV1ClientServiceImpl + extends LGThinQAbstractApiV1ClientService + implements LGThinQWMApiClientService { + private final Logger logger = LoggerFactory.getLogger(LGThinQWMApiV1ClientServiceImpl.class); + + protected LGThinQWMApiV1ClientServiceImpl(HttpClient httpClient) { + super(WasherDryerCapability.class, WasherDryerSnapshot.class, httpClient); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + // there's no before settings to send command + return false; + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + @Nullable + public WasherDryerSnapshot getDeviceData(String bridgeName, String deviceId, CapabilityDefinition capDef) { + throw new UnsupportedOperationException("Method not supported in V1 API device."); + } + + @Override + public void remoteStart(String bridgeName, WasherDryerCapability cap, String deviceId, Map data) + throws LGThinqApiException { + try { + CommandDefinition cmdStartDef = cap.getCommandsDefinition().get(cap.getCommandRemoteStart()); + if (cmdStartDef == null) { + logger.warn("No command definition found for remote start v1. Ignoring command"); + return; + } + Map cmdPayload = prepareCommandV1(cmdStartDef, data); + logger.debug("token Payload:[{}]", cmdPayload); + RestResult result = sendCommand(bridgeName, deviceId, cmdPayload); + handleGenericErrorResult(result); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error sending remote start", e); + } + } + + @Override + public void wakeUp(String bridgeName, String deviceId, Boolean wakeUp) throws LGThinqApiException { + try { + + RestResult result = sendCommand(bridgeName, deviceId, "", "Control", "Operation", "", "WakeUp"); + handleGenericErrorResult(result); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error sending remote start", e); + } + } + + @Override + protected Map prepareCommandV1(CommandDefinition cmdDef, Map snapData) + throws JsonProcessingException { + // expected map ordered here + String dataStr = cmdDef.getDataTemplate(); + for (Map.Entry e : snapData.entrySet()) { + String value = String.valueOf(e.getValue()); + if ("Start".equals(cmdDef.getCmdOptValue()) && e.getKey().equals("Option2")) { + // For some reason, option2 fills only InitialBit with 1. + value = "1"; + } + dataStr = dataStr.replace("{{" + e.getKey() + "}}", value); + } + // Keep the order + LinkedHashMap cmd = completeCommandDataNodeV1(cmdDef, dataStr); + cmd.remove("encode"); + + return cmd; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiV2ClientServiceImpl.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiV2ClientServiceImpl.java new file mode 100644 index 0000000000000..8444019dbb8b8 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQWMApiV2ClientServiceImpl.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WMD_COMMAND_REMOTE_START_V2; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.RestResult; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerCapability; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerSnapshot; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * The {@link LGThinQWMApiV2ClientServiceImpl} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinQWMApiV2ClientServiceImpl + extends LGThinQAbstractApiV2ClientService + implements LGThinQWMApiClientService { + + protected LGThinQWMApiV2ClientServiceImpl(HttpClient httpClient) { + super(WasherDryerCapability.class, WasherDryerSnapshot.class, httpClient); + } + + @Override + protected boolean beforeGetDataDevice(String bridgeName, String deviceId) { + return false; + } + + @Override + public void turnDevicePower(String bridgeName, String deviceId, DevicePowerState newPowerState) { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + public void remoteStart(String bridgeName, WasherDryerCapability cap, String deviceId, Map data) + throws LGThinqApiException { + try { + ObjectNode dataSetList = JsonNodeFactory.instance.objectNode(); + ObjectNode nodeData = dataSetList.putObject("dataSetList").putObject("washerDryer"); + // 1 - mount nodeData template + CommandDefinition cdStart = cap.getCommandsDefinition().get(WMD_COMMAND_REMOTE_START_V2); + if (cdStart == null) { + throw new LGThinqApiException( + "Command WMStart doesn't defined in cap. Do the Device support Remote Start ?"); + } + // remove data values (based on command template values) that it's not the real name + data.remove("course"); + data.remove("SmartCourse"); + for (Map.Entry value : data.entrySet()) { + Object v = value.getValue(); + if (v instanceof Double) { + nodeData.put(value.getKey(), (Double) v); + } else if (v instanceof Integer) { + nodeData.put(value.getKey(), (Integer) v); + } else { + nodeData.put(value.getKey(), value.getValue().toString()); + } + } + + RestResult result = sendCommand(bridgeName, deviceId, "control-sync", WMD_COMMAND_REMOTE_START_V2, "Set", + null, null, dataSetList); + handleGenericErrorResult(result); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error sending remote start", e); + } + } + + @Override + public void wakeUp(String bridgeName, String deviceId, Boolean wakeUp) throws LGThinqApiException { + try { + ObjectNode dataSetList = JsonNodeFactory.instance.objectNode(); + dataSetList.putObject("dataSetList").putObject("washerDryer").put("controlDataType", "WAKEUP") + .put("controlDataValueLength", wakeUp ? "1" : "0"); + + RestResult result = sendCommand(bridgeName, deviceId, "control-sync", "WMWakeup", "Set", null, null, + dataSetList); + handleGenericErrorResult(result); + } catch (LGThinqApiException e) { + throw e; + } catch (Exception e) { + throw new LGThinqApiException("Error sending remote start", e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqCanonicalModelUtil.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqCanonicalModelUtil.java new file mode 100644 index 0000000000000..d46624d5867f3 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqCanonicalModelUtil.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.api.model.GatewayResult; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link LGThinqCanonicalModelUtil} class - Utilities to help communication with LG API + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqCanonicalModelUtil { + public static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Get structured result from the LG Authentication Gateway + * + * @param rawJson RAW Json to process + * @return Structured Object returned from the API + * @throws IOException If some error happen procession token from file. + */ + public static GatewayResult getGatewayResult(String rawJson) throws IOException { + Map map = mapper.readValue(rawJson, new TypeReference<>() { + }); + @SuppressWarnings("unchecked") + Map content = (Map) map.get("result"); + String resultCode = (String) map.get("resultCode"); + if (content == null || content.isEmpty()) { + throw new IllegalArgumentException("Unexpected result. Gateway Content Result is null"); + } else if (resultCode == null) { + throw new IllegalArgumentException("Unexpected result. resultCode code is null"); + } + + return new GatewayResult(Objects.requireNonNull(resultCode, "Expected resultCode field in json"), "", + Objects.requireNonNull(content.get("rtiUri"), "Expected rtiUri field in json"), + Objects.requireNonNull(content.get("thinq1Uri"), "Expected thinq1Uri field in json"), + Objects.requireNonNull(content.get("thinq2Uri"), "Expected thinq2Uri field in json"), + Objects.requireNonNull(content.get("empUri"), "Expected empUri field in json"), + Objects.requireNonNull(content.get("empTermsUri"), "Expected empTermsUri field in json"), "", + Objects.requireNonNull(content.get("empSpxUri"), "Expected empSpxUri field in json")); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqGateway.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqGateway.java new file mode 100644 index 0000000000000..ce5a2f95e4b56 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqGateway.java @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_EMP_SESS_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_EMP_SESS_URL; + +import java.io.Serial; +import java.io.Serializable; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.api.model.GatewayResult; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * The {@link LGThinqGateway} hold information about the LG Gateway + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqGateway implements Serializable { + @Serial + private static final long serialVersionUID = 202409261421L; + private String empBaseUri = ""; + private String loginBaseUri = ""; + private String apiRootV1 = ""; + private String apiRootV2 = ""; + private String authBase = ""; + private String language = ""; + private String country = ""; + private String username = ""; + private String password = ""; + private String alternativeEmpServer = ""; + private int accountVersion; + + public LGThinqGateway() { + } + + public LGThinqGateway(GatewayResult gwResult, String language, String country, String alternativeEmpServer) { + this.apiRootV2 = gwResult.getThinq2Uri(); + this.apiRootV1 = gwResult.getThinq1Uri(); + this.loginBaseUri = gwResult.getEmpSpxUri(); + this.authBase = gwResult.getEmpUri(); + this.empBaseUri = gwResult.getEmpTermsUri(); + this.language = language; + this.country = country; + this.alternativeEmpServer = alternativeEmpServer; + } + + @JsonIgnore + public String getTokenSessionEmpUrl() { + return alternativeEmpServer.isBlank() ? LG_API_V2_EMP_SESS_URL : alternativeEmpServer + LG_API_V2_EMP_SESS_PATH; + } + + public String getEmpBaseUri() { + return empBaseUri; + } + + public int getAccountVersion() { + return accountVersion; + } + + public String getApiRootV2() { + return apiRootV2; + } + + public String getAuthBase() { + return authBase; + } + + public String getLanguage() { + return language; + } + + public String getCountry() { + return country; + } + + public String getLoginBaseUri() { + return loginBaseUri; + } + + public String getApiRootV1() { + return apiRootV1; + } + + public void setEmpBaseUri(String empBaseUri) { + this.empBaseUri = empBaseUri; + } + + public void setLoginBaseUri(String loginBaseUri) { + this.loginBaseUri = loginBaseUri; + } + + public void setApiRootV1(String apiRootV1) { + this.apiRootV1 = apiRootV1; + } + + public void setApiRootV2(String apiRootV2) { + this.apiRootV2 = apiRootV2; + } + + public void setAuthBase(String authBase) { + this.authBase = authBase; + } + + public void setLanguage(String language) { + this.language = language; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "LGThinqGateway{" + "empBaseUri='" + empBaseUri + '\'' + ", loginBaseUri='" + loginBaseUri + '\'' + + ", apiRootV1='" + apiRootV1 + '\'' + ", apiRootV2='" + apiRootV2 + '\'' + ", authBase='" + authBase + + '\'' + ", language='" + language + '\'' + ", country='" + country + '\'' + ", username='" + username + + '\'' + ", password='" + (!password.isEmpty() ? "******" : "") + '\'' + + ", alternativeEmpServer='" + alternativeEmpServer + '\'' + ", accountVersion=" + accountVersion + '}'; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqOauthEmpAuthenticator.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqOauthEmpAuthenticator.java new file mode 100644 index 0000000000000..966edd2af8c4b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/LGThinqOauthEmpAuthenticator.java @@ -0,0 +1,365 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.*; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TimeZone; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.api.model.GatewayResult; +import org.openhab.binding.lgthinq.lgservices.errors.RefreshTokenException; +import org.openhab.binding.lgthinq.lgservices.model.ResultCodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link LGThinqOauthEmpAuthenticator} main service to authenticate against LG Emp Server via Oauth + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqOauthEmpAuthenticator { + + private static final Logger logger = LoggerFactory.getLogger(LGThinqOauthEmpAuthenticator.class); + private static final Map oauthSearchKeyQueryParams = new LinkedHashMap<>(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + static { + oauthSearchKeyQueryParams.put("key_name", "OAUTH_SECRETKEY"); + oauthSearchKeyQueryParams.put("sever_type", "OP"); + } + + private final HttpClient httpClient; + + public LGThinqOauthEmpAuthenticator(HttpClient httpClient) { + this.httpClient = httpClient; + } + + record PreLoginResult(String username, String signature, String timestamp, String encryptedPwd) { + } + + record LoginAccountResult(String userIdType, String userId, String country, String loginSessionId) { + } + + private Map getGatewayRestHeader(String language, String country) { + return Map.ofEntries(new AbstractMap.SimpleEntry("Accept", "application/json"), + new AbstractMap.SimpleEntry<>("x-api-key", LG_API_API_KEY_V2), + new AbstractMap.SimpleEntry<>("x-country-code", country), + new AbstractMap.SimpleEntry<>("x-client-id", LG_API_CLIENT_ID), + new AbstractMap.SimpleEntry<>("x-language-code", language), + new AbstractMap.SimpleEntry<>("x-message-id", LG_API_MESSAGE_ID), + new AbstractMap.SimpleEntry<>("x-service-code", LG_API_SVC_CODE), + new AbstractMap.SimpleEntry<>("x-service-phase", LG_API_SVC_PHASE), + new AbstractMap.SimpleEntry<>("x-thinq-app-level", LG_API_APP_LEVEL), + new AbstractMap.SimpleEntry<>("x-thinq-app-os", LG_API_APP_OS), + new AbstractMap.SimpleEntry<>("x-thinq-app-type", LG_API_APP_TYPE), + new AbstractMap.SimpleEntry<>("x-thinq-app-ver", LG_API_APP_VER)); + } + + private Map getLoginHeader(LGThinqGateway gw) { + Map headers = new HashMap<>(); + headers.put("Connection", "keep-alive"); + headers.put("X-Device-Language-Type", "IETF"); + headers.put("X-Application-Key", "6V1V8H2BN5P9ZQGOI5DAQ92YZBDO3EK9"); + headers.put("X-Client-App-Key", "LGAO221A02"); + headers.put("X-Lge-Svccode", "SVC709"); + headers.put("X-Device-Type", "M01"); + headers.put("X-Device-Platform", "ADR"); + headers.put("X-Device-Publish-Flag", "Y"); + headers.put("X-Device-Country", gw.getCountry()); + headers.put("X-Device-Language", gw.getLanguage()); + headers.put("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Accept-Encoding", "gzip, deflate, br"); + headers.put("Accept-Language", "en-US,en;q=0.9,pt-BR;q=0.8,pt;q=0.7"); + headers.put("Accept", "application/json"); + return headers; + } + + LGThinqGateway discoverGatewayConfiguration(String gwUrl, String language, String country, + String alternativeEmpServer) throws IOException { + Map header = getGatewayRestHeader(language, country); + RestResult result; + result = RestUtils.getCall(httpClient, gwUrl, header, null); + + if (result.getStatusCode() != 200) { + throw new IllegalStateException( + String.format("Expected HTTP OK return, but received result core:[%s] - error message:[%s]", + result.getJsonResponse(), ResultCodes.getReasonResponse(result.getJsonResponse()))); + } else { + GatewayResult gwResult = LGThinqCanonicalModelUtil.getGatewayResult(result.getJsonResponse()); + ResultCodes resultCode = ResultCodes.fromCode(gwResult.getReturnedCode()); + if (ResultCodes.OK != resultCode) { + throw new IllegalStateException(String.format( + "Result from LGThinq Gateway from Authentication URL was unexpected. ResultCode: %s, with message:%s, Error Description:%s", + gwResult.getReturnedCode(), gwResult.getReturnedMessage(), resultCode.getDescription())); + } + + return new LGThinqGateway(gwResult, language, country, alternativeEmpServer); + } + } + + PreLoginResult preLoginUser(LGThinqGateway gw, String username, String password) throws IOException { + String encPwd = RestUtils.getPreLoginEncPwd(password); + Map headers = getLoginHeader(gw); + // 1) Doing preLogin -> getting the password key + String preLoginUrl = gw.getLoginBaseUri() + LG_API_PRE_LOGIN_PATH; + Map formData = Map.of("user_auth2", encPwd, "log_param", String.format( + "login request / user_id : %s / " + "third_party : null / svc_list : SVC202,SVC710 / 3rd_service : ", + username)); + RestResult resp = RestUtils.postCall(httpClient, preLoginUrl, headers, formData); + if (resp == null) { + throw new IllegalStateException("Error login into account. Null data returned"); + } else if (resp.getStatusCode() != 200) { + throw new IllegalStateException( + String.format("Error preLogin into account: The reason is: %s", resp.getJsonResponse())); + } + + Map preLoginResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + logger.debug("encrypted_pw={}, signature={}, tStamp={}", preLoginResult.get("encrypted_pw"), + preLoginResult.get("signature"), preLoginResult.get("tStamp")); + return new PreLoginResult(username, + Objects.requireNonNull(preLoginResult.get("signature"), + "Unexpected login json result. Node 'signature' not found"), + Objects.requireNonNull(preLoginResult.get("tStamp"), + "Unexpected login json result. Node 'signature' not found"), + Objects.requireNonNull(preLoginResult.get("encrypted_pw"), + "Unexpected login json result. Node 'signature' not found")); + } + + LoginAccountResult loginUser(LGThinqGateway gw, PreLoginResult preLoginResult) throws IOException { + // 2 - Login with username and hashed password + Map headers = getLoginHeader(gw); + headers.put("X-Signature", preLoginResult.signature()); + headers.put("X-Timestamp", preLoginResult.timestamp()); + Map formData = Map.of("user_auth2", preLoginResult.encryptedPwd(), + "password_hash_prameter_flag", "Y", "svc_list", "SVC202,SVC710"); // SVC202=LG SmartHome, SVC710=EMP + // OAuth + String loginUrl = gw.getEmpBaseUri() + LG_API_V2_SESSION_LOGIN_PATH + + URLEncoder.encode(preLoginResult.username(), StandardCharsets.UTF_8); + RestResult resp = RestUtils.postCall(httpClient, loginUrl, headers, formData); + if (resp == null) { + throw new IllegalStateException("Error loggin into acccount. Null data returned"); + } else if (resp.getStatusCode() != 200) { + throw new IllegalStateException( + String.format("Error login into account. The reason is: %s", resp.getJsonResponse())); + } + Map loginResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + @SuppressWarnings("unchecked") + Map accountResult = (Map) loginResult.get("account"); + if (accountResult == null) { + throw new IllegalStateException("Error getting account from Login"); + } + return new LoginAccountResult( + Objects.requireNonNull(accountResult.get("userIDType"), + "Unexpected account json result. 'userIDType' not found"), + Objects.requireNonNull(accountResult.get("userID"), + "Unexpected account json result. 'userID' not found"), + Objects.requireNonNull(accountResult.get("country"), + "Unexpected account json result. 'country' not found"), + Objects.requireNonNull(accountResult.get("loginSessionID"), + "Unexpected account json result. 'loginSessionID' not found")); + } + + private String getCurrentTimestamp() { + SimpleDateFormat sdf = new SimpleDateFormat(LG_API_DATE_FORMAT, Locale.US); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.format(new Date()); + } + + TokenResult getToken(LGThinqGateway gw, LoginAccountResult accountResult) throws IOException { + // 3 - get secret key from emp signature + String empSearchKeyUrl = gw.getLoginBaseUri() + LG_API_OAUTH_SEARCH_KEY_PATH; + + RestResult resp = RestUtils.getCall(httpClient, empSearchKeyUrl, null, oauthSearchKeyQueryParams); + if (resp.getStatusCode() != 200) { + throw new IllegalStateException( + String.format("Error loggin into acccount. The reason is:%s", resp.getJsonResponse())); + } + Map secretResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + String secretKey = Objects.requireNonNull(secretResult.get("returnData"), + "Unexpected json returned. Expected 'returnData' node here"); + logger.debug("Secret found:{}", secretResult.get("returnData")); + + // 4 - get OAuth Token Key from EMP API + Map empData = new LinkedHashMap<>(); + empData.put("account_type", accountResult.userIdType()); + empData.put("client_id", LG_API_CLIENT_ID); + empData.put("country_code", accountResult.country()); + empData.put("username", accountResult.userId()); + String timestamp = getCurrentTimestamp(); + + byte[] oauthSig = RestUtils.getTokenSignature(gw.getTokenSessionEmpUrl(), secretKey, empData, timestamp); + + Map oauthEmpHeaders = getOauthEmpHeaders(accountResult, timestamp, oauthSig); + logger.debug("===> Localized timestamp used: [{}]", timestamp); + logger.debug("===> signature created: [{}]", new String(oauthSig)); + resp = RestUtils.postCall(httpClient, gw.getTokenSessionEmpUrl(), oauthEmpHeaders, empData); + return handleTokenResult(resp); + } + + private Map getOauthEmpHeaders(LoginAccountResult accountResult, String timestamp, + byte[] oauthSig) { + Map oauthEmpHeaders = new LinkedHashMap<>(); + oauthEmpHeaders.put("lgemp-x-app-key", LG_API_OAUTH_CLIENT_KEY); + oauthEmpHeaders.put("lgemp-x-date", timestamp); + oauthEmpHeaders.put("lgemp-x-session-key", accountResult.loginSessionId()); + oauthEmpHeaders.put("lgemp-x-signature", new String(oauthSig)); + oauthEmpHeaders.put("Accept", "application/json"); + oauthEmpHeaders.put("X-Device-Type", "M01"); + oauthEmpHeaders.put("X-Device-Platform", "ADR"); + oauthEmpHeaders.put("Content-Type", "application/x-www-form-urlencoded"); + oauthEmpHeaders.put("Access-Control-Allow-Origin", "*"); + oauthEmpHeaders.put("Accept-Encoding", "gzip, deflate, br"); + oauthEmpHeaders.put("Accept-Language", "en-US,en;q=0.9"); + oauthEmpHeaders.put("User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44"); + return oauthEmpHeaders; + } + + UserInfo getUserInfo(TokenResult token) throws IOException { + UriBuilder builder = UriBuilder.fromUri(token.getOauthBackendUrl()).path(LG_API_V2_USER_INFO); + String oauthUrl = builder.build().toURL().toString(); + String timestamp = getCurrentTimestamp(); + byte[] oauthSig = RestUtils.getTokenSignature(oauthUrl, LG_API_OAUTH_SECRET_KEY, Collections.emptyMap(), + timestamp); + Map headers = Map.of("Accept", "application/json", "Authorization", + String.format("Bearer %s", token.getAccessToken()), "X-Lge-Svccode", LG_API_SVC_CODE, + "X-Application-Key", LG_API_APPLICATION_KEY, "lgemp-x-app-key", LG_API_CLIENT_ID, "X-Device-Type", + "M01", "X-Device-Platform", "ADR", "x-lge-oauth-date", timestamp, "x-lge-oauth-signature", + new String(oauthSig)); + RestResult resp = RestUtils.getCall(httpClient, oauthUrl, headers, null); + + return handleAccountInfoResult(resp); + } + + private UserInfo handleAccountInfoResult(RestResult resp) throws IOException { + Map result = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + if (resp.getStatusCode() != 200) { + throw new IllegalStateException( + String.format("LG API returned error when trying to get user account information. The reason is:%s", + resp.getJsonResponse())); + } else if (result.get("account") == null + || ((Map) result.getOrDefault("account", Collections.emptyMap())).get("userNo") == null) { + throw new IllegalStateException("Error retrieving the account user information from access token"); + } + @SuppressWarnings("unchecked") + Map accountInfo = (Map) result.getOrDefault("account", Collections.emptyMap()); + + return new UserInfo( + Objects.requireNonNullElse(accountInfo.get("userNo"), + "Unexpected result. userID must be present in json result"), + Objects.requireNonNull(accountInfo.get("userID"), + "Unexpected result. userID must be present in json result"), + Objects.requireNonNull(accountInfo.get("userIDType"), + "Unexpected result. userIDType must be present in json result"), + Objects.requireNonNullElse(accountInfo.get("displayUserID"), "")); + } + + TokenResult doRefreshToken(TokenResult currentToken) throws IOException, RefreshTokenException { + UriBuilder builder = UriBuilder.fromUri(currentToken.getOauthBackendUrl()).path(LG_API_V2_AUTH_PATH); + String oauthUrl = builder.build().toURL().toString(); + String timestamp = getCurrentTimestamp(); + + Map formData = new LinkedHashMap<>(); + formData.put("grant_type", "refresh_token"); + formData.put("refresh_token", currentToken.getRefreshToken()); + + byte[] oauthSig = RestUtils.getTokenSignature(oauthUrl, LG_API_OAUTH_SECRET_KEY, formData, timestamp); + + Map headers = Map.of("x-lge-appkey", LG_API_CLIENT_ID, "x-lge-oauth-signature", + new String(oauthSig), "x-lge-oauth-date", timestamp, "Accept", "application/json"); + + RestResult resp = RestUtils.postCall(httpClient, oauthUrl, headers, formData); + return handleRefreshTokenResult(resp, currentToken); + } + + private TokenResult handleTokenResult(@Nullable RestResult resp) throws IOException { + Map tokenResult; + if (resp == null) { + throw new IllegalStateException("Error getting oauth token. Null data returned"); + } + if (resp.getStatusCode() != 200) { + throw new IllegalStateException( + String.format("Error getting oauth token. HTTP Status Code is:%s, The reason is:%s", + resp.getStatusCode(), resp.getJsonResponse())); + } else { + tokenResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + Integer status = (Integer) tokenResult.get("status"); + if ((status != null && !"1".equals("" + status)) || tokenResult.get("expires_in") == null) { + throw new IllegalStateException(String.format("Status error getting token:%s", tokenResult)); + } + } + + return new TokenResult( + Objects.requireNonNull((String) tokenResult.get("access_token"), + "Unexpected result. access_token must be present in json result"), + Objects.requireNonNull((String) tokenResult.get("refresh_token"), + "Unexpected result. refresh_token must be present in json result"), + Integer.parseInt(Objects.requireNonNull((String) tokenResult.get("expires_in"), + "Unexpected result. expires_in must be present in json result")), + new Date(), Objects.requireNonNull((String) tokenResult.get("oauth2_backend_url"), + "Unexpected result. oauth2_backend_url must be present in json result")); + } + + private TokenResult handleRefreshTokenResult(@Nullable RestResult resp, TokenResult currentToken) + throws IOException, RefreshTokenException { + Map tokenResult; + if (resp == null) { + throw new RefreshTokenException("Error getting oauth token. Null data returned"); + } + if (resp.getStatusCode() != 200) { + throw new RefreshTokenException( + String.format("Error getting oauth token. HTTP Status Code is:%s, The reason is:%s", + resp.getStatusCode(), resp.getJsonResponse())); + } else { + tokenResult = objectMapper.readValue(resp.getJsonResponse(), new TypeReference<>() { + }); + if (tokenResult.get("access_token") == null || tokenResult.get("expires_in") == null) { + throw new RefreshTokenException(String.format("Status error get refresh token info:%s", tokenResult)); + } + } + + currentToken.setAccessToken(Objects.requireNonNull(tokenResult.get("access_token"), + "Unexpected error. Access Token must ever been provided by LG API")); + currentToken.setGeneratedTime(new Date()); + currentToken.setExpiresIn(Integer.parseInt(Objects.requireNonNull(tokenResult.get("expires_in"), + "Unexpected error. Access Token must ever been provided by LG API"))); + return currentToken; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/RestResult.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/RestResult.java new file mode 100644 index 0000000000000..1ae8c9545cc14 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/RestResult.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link RestResult} result from rest calls + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class RestResult { + private final String jsonResponse; + private final int resultCode; + + public RestResult(String jsonResponse, int resultCode) { + this.jsonResponse = jsonResponse; + this.resultCode = resultCode; + } + + public String getJsonResponse() { + return jsonResponse; + } + + public int getStatusCode() { + return resultCode; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/RestUtils.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/RestUtils.java new file mode 100644 index 0000000000000..cba66fe04a20e --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/RestUtils.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.HMAC_SHA1_ALGORITHM; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.MESSAGE_DIGEST_ALGORITHM; + +import java.math.BigInteger; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.FormContentProvider; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.util.Fields; +import org.openhab.core.i18n.CommunicationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RestUtils} rest utilities + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class RestUtils { + + private static final Logger logger = LoggerFactory.getLogger(RestUtils.class); + + public static String getPreLoginEncPwd(String pwdToEnc) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + logger.warn("The required algorithm is not available.", e); + throw new IllegalStateException("Unexpected error. SHA-512 algorithm must exists in JDK distribution", e); + } + digest.reset(); + digest.update(pwdToEnc.getBytes(StandardCharsets.UTF_8)); + + return String.format("%0128x", new BigInteger(1, digest.digest())); + } + + public static byte[] getOauth2Sig(String messageSign, String secret) { + byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + SecretKeySpec signingKey = new SecretKeySpec(secretBytes, HMAC_SHA1_ALGORITHM); + + try { + Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); + mac.init(signingKey); + return Base64.getEncoder().encode(mac.doFinal(messageSign.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException e) { + logger.debug("Unexpected error. SHA1 algorithm must exists in JDK distribution.", e); + throw new IllegalStateException("Unexpected error. SHA1 algorithm must exists in JDK distribution", e); + } catch (InvalidKeyException e) { + logger.debug("Unexpected error.", e); + throw new IllegalStateException("Unexpected error.", e); + } + } + + public static byte[] getTokenSignature(String authUrl, String secretKey, Map empData, + String timestamp) { + UriBuilder builder = UriBuilder.fromUri(authUrl); + empData.forEach(builder::queryParam); + + URI reqUri = builder.build(); + String signUrl = !empData.isEmpty() ? reqUri.getPath() + "?" + reqUri.getRawQuery() : reqUri.getPath(); + String messageToSign = String.format("%s\n%s", signUrl, timestamp); + return getOauth2Sig(messageToSign, secretKey); + } + + public static RestResult getCall(HttpClient httpClient, String encodedUrl, @Nullable Map headers, + @Nullable Map params) { + + Request request = httpClient.newRequest(encodedUrl).method("GET"); + if (params != null) { + params.forEach(request::param); + } + if (headers != null) { + headers.forEach(request::header); + } + + if (logger.isTraceEnabled()) { + logger.trace("GET request: {}", request.getURI()); + } + try { + ContentResponse response = request.send(); + + logger.trace("GET response: {}", response.getContentAsString()); + + return new RestResult(response.getContentAsString(), response.getStatus()); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Exception occurred during GET execution: {}", e.getMessage(), e); + throw new CommunicationException(e); + } + } + + @Nullable + public static RestResult postCall(HttpClient httpClient, String encodedUrl, Map headers, + String jsonData) { + return postCall(httpClient, encodedUrl, headers, new StringContentProvider(jsonData)); + } + + @Nullable + public static RestResult postCall(HttpClient httpClient, String encodedUrl, Map headers, + Map formParams) { + Fields fields = new Fields(); + formParams.forEach(fields::put); + return postCall(httpClient, encodedUrl, headers, new FormContentProvider(fields)); + } + + @Nullable + private static RestResult postCall(HttpClient httpClient, String encodedUrl, Map headers, + ContentProvider contentProvider) { + + try { + Request request = httpClient.newRequest(encodedUrl).method("POST").content(contentProvider).timeout(10, + TimeUnit.SECONDS); + headers.forEach(request::header); + logger.trace("POST request to URI: {}", request.getURI()); + + ContentResponse response = request.content(contentProvider).timeout(10, TimeUnit.SECONDS).send(); + + logger.trace("POST response: {}", response.getContentAsString()); + + return new RestResult(response.getContentAsString(), response.getStatus()); + } catch (TimeoutException e) { + logger.warn("Timeout reading post call result from LG API", e); // In SocketTimeout cases I'm considering + // that I have no response on time. Then, I + // return null data + // forcing caller to retry. + return null; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CommunicationException(e); + } catch (ExecutionException e) { + throw new CommunicationException(e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/TokenManager.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/TokenManager.java new file mode 100644 index 0000000000000..a09e29686dd8f --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/TokenManager.java @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.THINQ_CONNECTION_DATA_FILE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_GATEWAY_SERVICE_PATH_V2; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_GATEWAY_URL_V2; + +import java.io.File; +import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.lgthinq.lgservices.errors.AccountLoginException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqGatewayException; +import org.openhab.binding.lgthinq.lgservices.errors.PreLoginException; +import org.openhab.binding.lgthinq.lgservices.errors.RefreshTokenException; +import org.openhab.binding.lgthinq.lgservices.errors.TokenException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link TokenManager} Principal facade to manage all token handles + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class TokenManager { + private static final int EXPIRICY_TOLERANCE_SEC = 60; + private static final Logger logger = LoggerFactory.getLogger(TokenManager.class); + private final LGThinqOauthEmpAuthenticator authenticator; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final Map tokenCached = new ConcurrentHashMap<>(); + + public TokenManager(HttpClient httpClient) { + authenticator = new LGThinqOauthEmpAuthenticator(httpClient); + } + + public boolean isTokenExpired(TokenResult token) { + Calendar c = Calendar.getInstance(); + c.setTime(token.getGeneratedTime()); + c.add(Calendar.SECOND, token.getExpiresIn() - EXPIRICY_TOLERANCE_SEC); + Date expiricyDate = c.getTime(); + return expiricyDate.before(new Date()); + } + + public TokenResult refreshToken(String bridgeName, TokenResult currentToken) throws RefreshTokenException { + try { + TokenResult token = authenticator.doRefreshToken(currentToken); + objectMapper.writeValue(new File(getConfigDataFileName(bridgeName)), token); + return token; + } catch (IOException e) { + throw new RefreshTokenException("Error refreshing LGThinq token", e); + } + } + + private String getConfigDataFileName(String bridgeName) { + return String.format(THINQ_CONNECTION_DATA_FILE, bridgeName); + } + + public boolean isOauthTokenRegistered(String bridgeName) { + File tokenFile = new File(getConfigDataFileName(bridgeName)); + return tokenFile.isFile(); + } + + private String getGatewayUrl(String alternativeGtwServer) { + return alternativeGtwServer.isBlank() ? LG_API_GATEWAY_URL_V2 + : (alternativeGtwServer + LG_API_GATEWAY_SERVICE_PATH_V2); + } + + public void oauthFirstRegistration(String bridgeName, String language, String country, String username, + String password, String alternativeGtwServer) + throws LGThinqGatewayException, PreLoginException, AccountLoginException, TokenException, IOException { + LGThinqGateway gw; + LGThinqOauthEmpAuthenticator.PreLoginResult preLogin; + LGThinqOauthEmpAuthenticator.LoginAccountResult accountLogin; + TokenResult token; + UserInfo userInfo; + try { + gw = authenticator.discoverGatewayConfiguration(getGatewayUrl(alternativeGtwServer), language, country, + alternativeGtwServer); + } catch (Exception ex) { + throw new LGThinqGatewayException("Error trying to discover the LG Gateway Setting for the region informed", + ex); + } + + try { + preLogin = authenticator.preLoginUser(gw, username, password); + } catch (Exception ex) { + throw new PreLoginException("Error doing pre-login of the user in the Emp LG Server", ex); + } + try { + accountLogin = authenticator.loginUser(gw, preLogin); + } catch (Exception ex) { + throw new AccountLoginException("Error doing user's account login on the Emp LG Server", ex); + } + try { + token = authenticator.getToken(gw, accountLogin); + } catch (Exception ex) { + throw new TokenException("Error getting Token", ex); + } + try { + userInfo = authenticator.getUserInfo(token); + token.setUserInfo(userInfo); + token.setGatewayInfo(gw); + } catch (Exception ex) { + throw new TokenException("Error getting UserInfo from Token", ex); + } + + // persist the token information generated in file + objectMapper.writeValue(new File(getConfigDataFileName(bridgeName)), token); + } + + public TokenResult getValidRegisteredToken(String bridgeName) throws IOException, RefreshTokenException { + TokenResult validToken; + TokenResult bridgeToken = tokenCached.get(bridgeName); + if (bridgeToken == null) { + bridgeToken = Objects.requireNonNull( + objectMapper.readValue(new File(getConfigDataFileName(bridgeName)), TokenResult.class), + "Unexpected. Never null here"); + } + + if (!isValidToken(bridgeToken)) { + throw new RefreshTokenException( + "Token is not valid. Try to delete token file and disable/enable bridge to restart authentication process"); + } else { + tokenCached.put(bridgeName, bridgeToken); + } + + validToken = Objects.requireNonNull(bridgeToken, "Unexpected. Never null here"); + if (isTokenExpired(validToken)) { + validToken = refreshToken(bridgeName, validToken); + } + return validToken; + } + + private boolean isValidToken(@Nullable TokenResult token) { + return token != null && !token.getAccessToken().isBlank() && token.getExpiresIn() != 0 + && !token.getOauthBackendUrl().isBlank() && !token.getRefreshToken().isBlank(); + } + + /** + * Remove the toke file registered for the bridge. Must be called only if the bridge is removed + */ + public void cleanupTokenRegistry(String bridgeName) { + File f = new File(getConfigDataFileName(bridgeName)); + if (f.isFile()) { + if (!f.delete()) { + logger.warn("Can't delete token registry file {}", f.getName()); + } + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/TokenResult.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/TokenResult.java new file mode 100644 index 0000000000000..f87341f193581 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/TokenResult.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TokenResult} Hold information about token and related entities + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class TokenResult implements Serializable { + @Serial + private static final long serialVersionUID = 202409261447L; + private String accessToken = ""; + private String refreshToken = ""; + private int expiresIn; + private Date generatedTime = new Date(); + private String oauthBackendUrl = ""; + private UserInfo userInfo = new UserInfo(); + private LGThinqGateway gatewayInfo = new LGThinqGateway(); + + public TokenResult(String accessToken, String refreshToken, int expiresIn, Date generatedTime, + String ouathBackendUrl) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + this.generatedTime = generatedTime; + this.oauthBackendUrl = ouathBackendUrl; + } + + // This constructor will never be called by this. It only exists because of ObjectMapper instantiation needs + public TokenResult() { + } + + public LGThinqGateway getGatewayInfo() { + return gatewayInfo; + } + + public void setGatewayInfo(LGThinqGateway gatewayInfo) { + this.gatewayInfo = gatewayInfo; + } + + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public int getExpiresIn() { + return expiresIn; + } + + public Date getGeneratedTime() { + return generatedTime; + } + + public String getOauthBackendUrl() { + return oauthBackendUrl; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public void setExpiresIn(int expiresIn) { + this.expiresIn = expiresIn; + } + + public void setGeneratedTime(Date generatedTime) { + this.generatedTime = generatedTime; + } + + public void setOauthBackendUrl(String ouathBackendUrl) { + this.oauthBackendUrl = ouathBackendUrl; + } + + public UserInfo getUserInfo() { + return userInfo; + } + + public void setUserInfo(UserInfo userInfo) { + this.userInfo = userInfo; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/UserInfo.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/UserInfo.java new file mode 100644 index 0000000000000..24498e0c369bc --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/UserInfo.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api; + +import java.io.Serial; +import java.io.Serializable; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link UserInfo} User Info (registered in LG Account) + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class UserInfo implements Serializable { + @Serial + private static final long serialVersionUID = 202409261445L; + private String userNumber = ""; + private String userID = ""; + private String userIDType = ""; + private String displayUserID = ""; + + public UserInfo() { + } + + public UserInfo(String userNumber, String userID, String userIDType, String displayUserId) { + this.userNumber = userNumber; + this.userID = userID; + this.userIDType = userIDType; + this.displayUserID = displayUserId; + } + + public String getUserNumber() { + return userNumber; + } + + public void setUserNumber(String userNumber) { + this.userNumber = userNumber; + } + + public String getUserID() { + return userID; + } + + public void setUserID(String userID) { + this.userID = userID; + } + + public String getUserIDType() { + return userIDType; + } + + public void setUserIDType(String userIDType) { + this.userIDType = userIDType; + } + + public String getDisplayUserID() { + return displayUserID; + } + + public void setDisplayUserID(String displayUserID) { + this.displayUserID = displayUserID; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/model/GatewayResult.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/model/GatewayResult.java new file mode 100644 index 0000000000000..45a8f56fd98f9 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/model/GatewayResult.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GatewayResult} class + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class GatewayResult extends HeaderResult { + private final String rtiUri; + private final String thinq1Uri; + private final String thinq2Uri; + private final String empUri; + private final String empTermsUri; + private final String oauthUri; + private final String empSpxUri; + + public GatewayResult(String resultCode, String resultMessage, String rtiUri, String thinq1Uri, String thinq2Uri, + String empUri, String empTermsUri, String oauthUri, String empSpxUri) { + super(resultCode, resultMessage); + this.rtiUri = rtiUri; + this.thinq1Uri = thinq1Uri; + this.thinq2Uri = thinq2Uri; + this.empUri = empUri; + this.empTermsUri = empTermsUri; + this.oauthUri = oauthUri; + this.empSpxUri = empSpxUri; + } + + public String getRtiUri() { + return rtiUri; + } + + public String getEmpTermsUri() { + return empTermsUri; + } + + public String getEmpSpxUri() { + return empSpxUri; + } + + public String getThinq1Uri() { + return thinq1Uri; + } + + public String getThinq2Uri() { + return thinq2Uri; + } + + public String getEmpUri() { + return empUri; + } + + public String getOauthUri() { + return oauthUri; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/model/HeaderResult.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/model/HeaderResult.java new file mode 100644 index 0000000000000..039106a33fd6f --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/api/model/HeaderResult.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.api.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link HeaderResult} class + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class HeaderResult { + private final String returnedCode; + private final String returnedMessage; + + public HeaderResult(String returnedCode, String returnedMessage) { + this.returnedCode = returnedCode; + this.returnedMessage = returnedMessage; + } + + public String getReturnedCode() { + return returnedCode; + } + + public String getReturnedMessage() { + return returnedMessage; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/AccountLoginException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/AccountLoginException.java new file mode 100644 index 0000000000000..58448b06a7d45 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/AccountLoginException.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AccountLoginException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class AccountLoginException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public AccountLoginException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqAccessException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqAccessException.java new file mode 100644 index 0000000000000..93911b25de728 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqAccessException.java @@ -0,0 +1,21 @@ +package org.openhab.binding.lgthinq.lgservices.errors; + +import org.openhab.binding.lgthinq.lgservices.model.ResultCodes; + +public class LGThinqAccessException extends LGThinqApiException { + public LGThinqAccessException(String message, Throwable cause) { + super(message, cause); + } + + public LGThinqAccessException(String message, Throwable cause, ResultCodes reasonCode) { + super(message, cause, reasonCode); + } + + public LGThinqAccessException(String message) { + super(message); + } + + public LGThinqAccessException(String message, ResultCodes resultCode) { + super(message, resultCode); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqApiException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqApiException.java new file mode 100644 index 0000000000000..76d78d2d3dc0b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqApiException.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.ResultCodes; + +/** + * The {@link LGThinqApiException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqApiException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261451L; + protected ResultCodes apiReasonCode = ResultCodes.UNKNOWN; + + public LGThinqApiException(String message, Throwable cause) { + super(message, cause); + } + + public LGThinqApiException(String message, Throwable cause, ResultCodes reasonCode) { + super(message, cause); + this.apiReasonCode = reasonCode; + } + + public ResultCodes getApiReasonCode() { + return apiReasonCode; + } + + public LGThinqApiException(String message) { + super(message); + } + + public LGThinqApiException(String message, ResultCodes resultCode) { + super(message); + this.apiReasonCode = resultCode; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqApiExhaustionException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqApiExhaustionException.java new file mode 100644 index 0000000000000..3d971bd33d492 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqApiExhaustionException.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGThinqApiExhaustionException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqApiExhaustionException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261451L; + + public LGThinqApiExhaustionException(String message, Throwable cause) { + super(message, cause); + } + + public LGThinqApiExhaustionException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqDeviceV1MonitorExpiredException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqDeviceV1MonitorExpiredException.java new file mode 100644 index 0000000000000..e22ba56f4eaae --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqDeviceV1MonitorExpiredException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGThinqDeviceV1MonitorExpiredException} - Normally caught by V1 API in monitoring device. + * After long-running moniotor, it indicates the need to refresh the monitor. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqDeviceV1MonitorExpiredException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public LGThinqDeviceV1MonitorExpiredException(String message, Throwable cause) { + super(message, cause); + } + + public LGThinqDeviceV1MonitorExpiredException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqDeviceV1OfflineException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqDeviceV1OfflineException.java new file mode 100644 index 0000000000000..8bb4639b39d6f --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqDeviceV1OfflineException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGThinqDeviceV1OfflineException} - Normally caught by V1 API in monitoring device. + * When the device is OFFLINE (away from internet), the API doesn't return data information and this + * exception is thrown to indicate that this device is offline for monitoring + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqDeviceV1OfflineException extends LGThinqApiException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public LGThinqDeviceV1OfflineException(String message, Throwable cause) { + super(message, cause); + } + + public LGThinqDeviceV1OfflineException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqException.java new file mode 100644 index 0000000000000..5f5a8fdc0ba0b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqException.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGThinqException} Parent Exception for all exceptions of this module + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqException extends Exception { + @Serial + private static final long serialVersionUID = 202409261450L; + + public LGThinqException(String message, Throwable cause) { + super(message, cause); + } + + public LGThinqException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqGatewayException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqGatewayException.java new file mode 100644 index 0000000000000..803e091317e50 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqGatewayException.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGThinqGatewayException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqGatewayException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public LGThinqGatewayException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqUnmarshallException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqUnmarshallException.java new file mode 100644 index 0000000000000..7fc08f2133f7d --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/LGThinqUnmarshallException.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGThinqUnmarshallException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class LGThinqUnmarshallException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public LGThinqUnmarshallException(String message, Throwable cause) { + super(message, cause); + } + + public LGThinqUnmarshallException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/PreLoginException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/PreLoginException.java new file mode 100644 index 0000000000000..cf50e978b1644 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/PreLoginException.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PreLoginException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class PreLoginException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public PreLoginException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/RefreshTokenException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/RefreshTokenException.java new file mode 100644 index 0000000000000..0e7c57e38e169 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/RefreshTokenException.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PreLoginException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class RefreshTokenException extends LGThinqApiException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public RefreshTokenException(String message, Throwable cause) { + super(message, cause); + } + + public RefreshTokenException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/TokenException.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/TokenException.java new file mode 100644 index 0000000000000..f7739df401c60 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/errors/TokenException.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.errors; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PreLoginException} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class TokenException extends LGThinqException { + @Serial + private static final long serialVersionUID = 202409261450L; + + public TokenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractCapability.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractCapability.java new file mode 100644 index 0000000000000..0dfcf58dae05e --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractCapability.java @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import static org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition.NULL_DEFINITION; + +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * The {@link AbstractCapability} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@SuppressWarnings("unchecked") +public abstract class AbstractCapability implements CapabilityDefinition { + // Define if the device supports sending setup commands before monitoring + // This is to control result 400 for some devices that doesn't support or permit setup commands before monitoring + boolean isBeforeCommandSupporter = true; + + public boolean isBeforeCommandSupported() { + return isBeforeCommandSupporter; + } + + public void setBeforeCommandSupported(boolean beforeCommandSupporter) { + isBeforeCommandSupporter = beforeCommandSupporter; + } + + // default result format + protected Map> featureDefinitionMap = new HashMap<>(); + + protected String modelName = ""; + final Class realClass; + + @Override + public String getModelName() { + return modelName; + } + + @Override + public void setModelName(String modelName) { + this.modelName = modelName; + } + + protected AbstractCapability() { + this.realClass = (Class) ((ParameterizedType) Objects.requireNonNull(getClass().getGenericSuperclass())) + .getActualTypeArguments()[0]; + } + + protected DeviceTypes deviceType = DeviceTypes.UNKNOWN; + protected LGAPIVerion version = LGAPIVerion.UNDEF; + private MonitoringResultFormat monitoringDataFormat = MonitoringResultFormat.UNKNOWN_FORMAT; + + private List monitoringBinaryProtocol = new ArrayList<>(); + + @Override + public MonitoringResultFormat getMonitoringDataFormat() { + return monitoringDataFormat; + } + + @Override + public void setMonitoringDataFormat(MonitoringResultFormat monitoringDataFormat) { + this.monitoringDataFormat = monitoringDataFormat; + } + + public void setFeatureDefinitionMap(Map> featureDefinitionMap) { + this.featureDefinitionMap = featureDefinitionMap; + } + + @Override + public List getMonitoringBinaryProtocol() { + return monitoringBinaryProtocol; + } + + @Override + public void setMonitoringBinaryProtocol(List monitoringBinaryProtocol) { + this.monitoringBinaryProtocol = monitoringBinaryProtocol; + } + + @Override + public void setDeviceType(DeviceTypes deviceType) { + this.deviceType = deviceType; + } + + @Override + public void setDeviceVersion(LGAPIVerion version) { + this.version = version; + } + + @Override + public DeviceTypes getDeviceType() { + return deviceType; + } + + @Override + public LGAPIVerion getDeviceVersion() { + return version; + } + + private Map rawData = new HashMap<>(); + + @JsonIgnore + public Map getRawData() { + return rawData; + } + + public Map> getFeatureValuesRawData() { + switch (getDeviceVersion()) { + case V1_0: + return Objects.requireNonNullElse((Map>) getRawData().get("Value"), + Collections.emptyMap()); + case V2_0: + return Objects.requireNonNullElse( + (Map>) getRawData().get("MonitoringValue"), Collections.emptyMap()); + default: + throw new IllegalStateException("Invalid version 'UNDEF' to get capability feature monitoring values"); + } + } + + public void setRawData(Map rawData) { + this.rawData = rawData; + } + + @Override + public FeatureDefinition getFeatureDefinition(String featureName) { + Function f = featureDefinitionMap.get(featureName); + return f != null ? f.apply(realClass.cast(this)) : NULL_DEFINITION; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractCapabilityFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractCapabilityFactory.java new file mode 100644 index 0000000000000..be0ab8a11b1ab --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractCapabilityFactory.java @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * The {@link AbstractCapability} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractCapabilityFactory { + protected final ObjectMapper mapper = new ObjectMapper(); + private static final Logger logger = LoggerFactory.getLogger(AbstractCapabilityFactory.class); + + public T create(JsonNode rootNode) throws LGThinqException { + T cap = getCapabilityInstance(); + cap.setModelName(rootNode.path("Info").path("modelName").textValue()); + cap.setDeviceType(ModelUtils.getDeviceType(rootNode)); + cap.setDeviceVersion(ModelUtils.discoveryAPIVersion(rootNode)); + cap.setRawData(mapper.convertValue(rootNode, new TypeReference<>() { + })); + switch (cap.getDeviceVersion()) { + case V1_0: + // V1 has Monitoring node describing the protocol data format + JsonNode type = rootNode.path(getMonitoringNodeName()).path("type"); + if (!type.isMissingNode() && type.isTextual()) { + cap.setMonitoringDataFormat(MonitoringResultFormat.getFormatOf(type.textValue())); + } + break; + case V2_0: + // V2 doesn't have node describing the protocol because it's they unified Value (features) and + // Monitoring nodes in the MonitoringValue node + cap.setMonitoringDataFormat(MonitoringResultFormat.JSON_FORMAT); + break; + default: + cap.setMonitoringDataFormat(MonitoringResultFormat.UNKNOWN_FORMAT); + } + if (MonitoringResultFormat.BINARY_FORMAT.equals(cap.getMonitoringDataFormat())) { + // get MonitorProtocol + JsonNode protocol = rootNode.path(getMonitoringNodeName()).path("protocol"); + if (protocol.isArray()) { + ArrayNode pNode = (ArrayNode) protocol; + List protocols = mapper.convertValue(pNode, new TypeReference<>() { + }); + cap.setMonitoringBinaryProtocol(protocols); + } else { + if (protocol.isMissingNode()) { + logger.warn("protocol node is missing in the capability descriptor for a binary monitoring"); + } else { + logger.warn("protocol node is not and array in the capability descriptor for a binary monitoring "); + } + } + } + return cap; + } + + /** + * Return constant pointing to MonitoringNode. This node has information about monitoring response description, + * only present in V1 devices. If some device has different node name for this descriptor, please override + * it. + * + * @return Monitoring node name + */ + protected String getMonitoringNodeName() { + return "Monitoring"; + } + + protected abstract List getSupportedDeviceTypes(); + + protected abstract List getSupportedAPIVersions(); + + /** + * Return the feature definition, i.e, the definition of the device attributes that can be mapped to Channels. + * The targetChannelId is needed if you intend to get the destination channelId for that feature, typically for + * dynamic channels. + * + * @param featureName Name of the features: feature node name + * @param featuresNode The jsonNode containing the data definition of the feature + * @param targetChannelId The destination channelID, normally used when you want to create dynamic channels (outside + * xml) + * @param refChannelId + * @return the Feature definition. + */ + protected abstract FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode, + @Nullable String targetChannelId, @Nullable String refChannelId); + + protected FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode) { + return newFeatureDefinition(featureName, featuresNode, null, null); + } + + protected abstract T getCapabilityInstance(); + + protected void validateMandatoryNote(JsonNode node) throws LGThinqException { + if (node.isMissingNode()) { + throw new LGThinqApiException( + String.format("Error extracting mandatory %s node for this device cap file", node)); + } + } + + protected abstract Map getCommandsDefinition(JsonNode rootNode); + + /** + * General method to parse commands for average of V1 Thinq Devices. + * + * @param rootNode ControlWifi root node + * @return return map with commands definition + */ + protected Map getCommandsDefinitionV1(JsonNode rootNode) { + boolean isBinaryCommands = MonitoringResultFormat.BINARY_FORMAT.getFormat() + .equals(rootNode.path("ControlWifi").path("type").textValue()); + JsonNode commandNode = rootNode.path("ControlWifi").path("action"); + if (commandNode.isMissingNode()) { + logger.warn("No commands found in the devices's definition. This is most likely a bug."); + return Collections.emptyMap(); + } + Map commands = new HashMap<>(); + for (Iterator> it = commandNode.fields(); it.hasNext();) { + Map.Entry e = it.next(); + String commandName = e.getKey(); + CommandDefinition cd = new CommandDefinition(); + JsonNode thisCommandNode = e.getValue(); + JsonNode cmdField = thisCommandNode.path("cmd"); + if (cmdField.isMissingNode()) { + // command not supported + continue; + } + cd.setCommand(cmdField.textValue()); + // cd.setCmdOpt(thisCommandNode.path("cmdOpt").textValue()); + cd.setCmdOptValue(thisCommandNode.path("value").textValue()); + cd.setBinary(isBinaryCommands); + String strData = Objects.requireNonNullElse(thisCommandNode.path("data").textValue(), ""); + cd.setDataTemplate(strData); + cd.setRawCommand(thisCommandNode.toPrettyString()); + int reservedIndex = 0; + // keep the order + if (!strData.isEmpty()) { + Map data = new LinkedHashMap<>(); + for (String f : strData.split(",")) { + if (f.contains("{")) { + // it's a featured field + // create data entry with the key and blank value + data.put(f.replaceAll("[{\\[}\\]]", ""), ""); + } else { + // its a fixed reserved value + data.put("Reserved" + reservedIndex, f.replaceAll("[{\\[}\\]]", "")); + reservedIndex++; + } + } + cd.setData(data); + } + commands.put(commandName, cd); + } + return commands; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractSnapshotDefinition.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractSnapshotDefinition.java new file mode 100644 index 0000000000000..b41470f5d9f5b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/AbstractSnapshotDefinition.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * The {@link AbstractSnapshotDefinition} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractSnapshotDefinition implements SnapshotDefinition { + + protected final Map otherInfo = new HashMap<>(); + + @JsonAnySetter + public void addOtherInfo(String propertyKey, Object value) { + this.otherInfo.put(propertyKey, value); + } + + @Nullable + public Object getOtherInfo(String propertyKey) { + return this.otherInfo.get(propertyKey); + } + + private Map rawData = new HashMap<>(); + + @JsonIgnore + public Map getRawData() { + return rawData; + } + + public void setRawData(Map rawData) { + this.rawData = rawData; + } + + public static final AbstractSnapshotDefinition EMPTY_SHOT = new AbstractSnapshotDefinition() { + @Override + public DevicePowerState getPowerStatus() { + return DevicePowerState.DV_POWER_UNK; + } + + @Override + public void setPowerStatus(DevicePowerState value) { + } + + @Override + public boolean isOnline() { + return false; + } + + @Override + public void setOnline(boolean online) { + } + }; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityDefinition.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityDefinition.java new file mode 100644 index 0000000000000..03888b71990b0 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityDefinition.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CapabilityDefinition} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface CapabilityDefinition { + boolean isBeforeCommandSupported(); + + void setBeforeCommandSupported(boolean supports); + + String getModelName(); + + void setModelName(String modelName); + + MonitoringResultFormat getMonitoringDataFormat(); + + void setMonitoringDataFormat(MonitoringResultFormat monitoringDataFormat); + + List getMonitoringBinaryProtocol(); + + void setMonitoringBinaryProtocol(List monitoringBinaryProtocol); + + DeviceTypes getDeviceType(); + + LGAPIVerion getDeviceVersion(); + + void setDeviceType(DeviceTypes deviceType); + + void setDeviceVersion(LGAPIVerion version); + + Map getRawData(); + + Map> getFeatureValuesRawData(); + + /** + * This method get the feature based on its name in the JSON device's definition. + * Ex: For V2: "MonitoringValue": { + * ... + * "spin" : { + * ... + * valueMapping{ + * ... + * } + * } + * } + * getFeatureDefinition("spin") will return the FeatureDefinition object representing "spin" feature configuration. + * + * @param featureName name of the feature node in the json definition + * @return return FeatureDefinition object representing the feature in case. + */ + FeatureDefinition getFeatureDefinition(String featureName); + + void setRawData(Map rawData); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityFactory.java new file mode 100644 index 0000000000000..43e21abb74e24 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityFactory.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCapabilityFactoryV1; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCapabilityFactoryV2; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherCapabilityFactoryV2; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCapabilityFactoryV1; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCapabilityFactoryV2; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerCapabilityFactoryV1; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerCapabilityFactoryV2; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link CapabilityFactory} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class CapabilityFactory { + final Map>> capabilityDeviceFactories = new HashMap<>(); + + private CapabilityFactory() { + List> factories = Arrays.asList(new ACCapabilityFactoryV1(), + new ACCapabilityFactoryV2(), new FridgeCapabilityFactoryV1(), new FridgeCapabilityFactoryV2(), + new WasherDryerCapabilityFactoryV1(), new WasherDryerCapabilityFactoryV2(), + new DishWasherCapabilityFactoryV2()); + factories.forEach(f -> { + f.getSupportedDeviceTypes().forEach(d -> { + Map> versionMap = capabilityDeviceFactories.get(d); + if (versionMap == null) { + versionMap = new HashMap<>(); + } + for (LGAPIVerion v : f.getSupportedAPIVersions()) { + versionMap.put(v, f); + } + capabilityDeviceFactories.put(d, versionMap); + }); + }); + } + + private static final CapabilityFactory instance; + static { + instance = new CapabilityFactory(); + } + private static final Logger logger = LoggerFactory.getLogger(CapabilityFactory.class); + + public static CapabilityFactory getInstance() { + return instance; + } + + public C create(JsonNode rootNode, Class clazz) throws LGThinqException { + DeviceTypes type = ModelUtils.getDeviceType(rootNode); + LGAPIVerion version = ModelUtils.discoveryAPIVersion(rootNode); + logger.info("Getting factory for device type:{} and version:{}", type.deviceTypeId(), version); + Map> versionsFactory = capabilityDeviceFactories + .get(type); + if (versionsFactory == null || versionsFactory.isEmpty()) { + throw new IllegalStateException("Unexpected capability. The type " + type + " was not implemented yet"); + } + AbstractCapabilityFactory factory = versionsFactory.get(version); + if (factory == null) { + throw new IllegalStateException( + "Unexpected capability. The type " + type + " and version " + version + " was not implemented yet"); + } + return clazz.cast(factory.create(rootNode)); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CommandDefinition.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CommandDefinition.java new file mode 100644 index 0000000000000..680e93f7c0373 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/CommandDefinition.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CommandDefinition} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class CommandDefinition { + /** + * This is the command tag value that is used by the API to launch the command service + */ + private String command = ""; + private Map data = new HashMap<>(); + + // =========== Used only for thinq V1 commands ============= + private String cmdOptValue = ""; + private boolean isBinary; + // This is the template in the device definition of data that must be send to the LG API complementing the command + private String dataTemplate = ""; + /* + * holds how command (in text) as defined in the node command definition. Ex: For Remote Start (WM): + * { + * "cmd":"Control", + * "cmdOpt":"Operation", + * "value":"Start", + * "data": + * "[{{Course}},{{Wash}},{{SpinSpeed}},{{WaterTemp}},{{RinseOption}},0,{{Reserve_Time_H}},{{Reserve_Time_M}},{{LoadItem}},{{Option1}},{{Option2}},0,{{SmartCourse}},0]", + * "encode":true + * } + */ + private String rawCommand = ""; + + // ========================================================= + + public String getRawCommand() { + return rawCommand; + } + + public void setRawCommand(String rawCommand) { + this.rawCommand = rawCommand; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } + + public String getCmdOptValue() { + return cmdOptValue; + } + + public void setCmdOptValue(String cmdOptValue) { + this.cmdOptValue = cmdOptValue; + } + + public boolean isBinary() { + return isBinary; + } + + public void setBinary(boolean binary) { + isBinary = binary; + } + + public String getDataTemplate() { + return dataTemplate; + } + + public void setDataTemplate(String dataTemplate) { + this.dataTemplate = dataTemplate; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DefaultSnapshotBuilder.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DefaultSnapshotBuilder.java new file mode 100644 index 0000000000000..b8e3290ae8054 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DefaultSnapshotBuilder.java @@ -0,0 +1,292 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link DefaultSnapshotBuilder} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class DefaultSnapshotBuilder implements SnapshotBuilder { + protected final Class snapClass; + protected static final ObjectMapper objectMapper = new ObjectMapper(); + private static final Logger logger = LoggerFactory.getLogger(DefaultSnapshotBuilder.class); + + public DefaultSnapshotBuilder(Class clazz) { + snapClass = clazz; + } + + private static final Map>> modelsCachedBitKeyDefinitions = new HashMap<>(); + + /** + * Create a Snapshot result based on snapshotData collected from LG API (V1/C2) + * + * @param binaryData V1: decoded returnedData + * @param capDef Capability Definition + * @return returns Snapshot implementation based on device type provided + * @throws LGThinqApiException any error. + */ + @Override + @SuppressWarnings("null") + public S createFromBinary(String binaryData, List prot, CapabilityDefinition capDef) + throws LGThinqUnmarshallException, LGThinqApiException { + try { + Map snapValues = new HashMap<>(); + byte[] data = binaryData.getBytes(); + BeanInfo beanInfo = Introspector.getBeanInfo(snapClass); + S snap = snapClass.getConstructor().newInstance(); + PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); + Map aliasesMethod = new HashMap<>(); + for (PropertyDescriptor property : pds) { + // all attributes of class. + Method m = property.getReadMethod(); // getter + if (m.isAnnotationPresent(JsonProperty.class)) { + String value = m.getAnnotation(JsonProperty.class).value(); + aliasesMethod.putIfAbsent(value, property); + } + if (m.isAnnotationPresent(JsonAlias.class)) { + @SuppressWarnings("null") + String[] values = m.getAnnotation(JsonAlias.class).value(); + for (String v : values) { + aliasesMethod.putIfAbsent(v, property); + } + } + } + for (MonitoringBinaryProtocol protField : prot) { + if (protField.startByte + protField.length > data.length) { + // end of data. If have more fields in the protocol, will be ignored + break; + } + String fName = protField.fieldName; + int value = 0; + for (int i = protField.startByte; i < protField.startByte + protField.length; i++) { + value = (value << 8) + data[i]; + } + snapValues.put(fName, value); + PropertyDescriptor property = aliasesMethod.get(fName); + if (property != null) { + // found property. Get bit value + Method m = property.getWriteMethod(); + if (m.getParameters()[0].getType() == String.class) { + m.invoke(snap, String.valueOf(value)); + } else if (m.getParameters()[0].getType() == Double.class) { + m.invoke(snap, (double) value); + } else if (m.getParameters()[0].getType() == Integer.class) { + m.invoke(snap, value); + } else { + throw new IllegalArgumentException( + String.format("Parameter type not supported for this factory:%s", + m.getParameters()[0].getType().toString())); + } + } + } + snap.setRawData(snapValues); + return snap; + } catch (IntrospectionException | InvocationTargetException | InstantiationException | IllegalAccessException + | NoSuchMethodException e) { + throw new LGThinqUnmarshallException("Unexpected Error unmarshalling binary data", e); + } + } + + /** + * Create a Snapshot result based on snapshotData collected from LG API (V1/C2) + * + * @param snapshotDataJson V1: decoded returnedData; V2: snapshot body + * @param deviceType device type + * @return returns Snapshot implementation based on device type provided + * @throws LGThinqApiException any error. + */ + @Override + public S createFromJson(String snapshotDataJson, DeviceTypes deviceType, CapabilityDefinition capDef) + throws LGThinqUnmarshallException, LGThinqApiException { + try { + Map snapshotMap = objectMapper.readValue(snapshotDataJson, new TypeReference<>() { + }); + Map deviceSetting = new HashMap<>(); + deviceSetting.put("deviceType", deviceType.deviceTypeId()); + deviceSetting.put("snapshot", snapshotMap); + return createFromJson(deviceSetting, capDef); + } catch (JsonProcessingException e) { + throw new LGThinqUnmarshallException("Unexpected Error unmarshalling json to map", e); + } + } + + @Override + public S createFromJson(Map deviceSettings, CapabilityDefinition capDef) + throws LGThinqApiException { + Map snapMap = objectMapper.convertValue(deviceSettings.get("snapshot"), new TypeReference<>() { + }); + if (snapMap == null) { + throw new LGThinqApiException("snapshot node not present in device monitoring result."); + } + return getSnapshot(snapMap, capDef); + } + + protected abstract S getSnapshot(Map snapMap, CapabilityDefinition capDef); + + protected DeviceTypes getDeviceType(Map rootMap) { + Integer deviceTypeId = (Integer) rootMap.get("deviceType"); + // device code is only present in v2 devices snapshot. + String deviceCode = Objects.requireNonNullElse((String) rootMap.get("deviceCode"), ""); + Objects.requireNonNull(deviceTypeId, "Unexpected error. deviceType field not present in snapshot schema"); + return DeviceTypes.fromDeviceTypeId(deviceTypeId, deviceCode); + } + + /** + * Create the map containing the bit representation of device features + * + * @param key raw value + * @param capFeatureValues capability features defined to the device + * @param cachedBitKey chached bitKey representation if any was done previously + * @return the bitKey - map os key features, position and options available + */ + private Map getBitKey(String key, final Map> capFeatureValues, + final Map> cachedBitKey) { + // Define a local function to search for the bit key + Function>, Map> searchBitKey = data -> { + if (data.isEmpty()) { + return Collections.emptyMap(); + } + + for (int i = 1; i <= 3; i++) { + String optKey = "Option" + i; + Map option = data.get(optKey); + + if (option == null) { + continue; + } + + List> optionList = objectMapper.convertValue(option.get("option"), + new TypeReference<>() { + }); + + if (optionList == null) { + continue; + } + + for (Map opt : optionList) { + String value = (String) opt.get("value"); + + if (key.equals(value)) { + Integer startBit = (Integer) opt.get("startbit"); + Integer length = (Integer) opt.getOrDefault("length", 1); + + if (startBit == null) { + return Collections.emptyMap(); + } + + Map bitKey = new HashMap<>(); + bitKey.put("option", optKey); + bitKey.put("startbit", startBit); + bitKey.put("length", length); + + return bitKey; + } + } + } + + return Collections.emptyMap(); + }; + + Map bitKey = cachedBitKey.get(key); + + if (bitKey == null) { + // cache the bitKey if it doesn't was fetched yet. + bitKey = searchBitKey.apply(capFeatureValues); + cachedBitKey.put(key, bitKey); + } + + return bitKey; + } + + /** + * Return the value related to the bit-value definition. It's used in Washer/Dryer V1 snapshot parser. + * It was here, in the parent, because maybe other devices need the same functionality. If not, + * We can transfer these methods to the WasherDryer Snapshot Builder. + * + * @param key Key trying to get the value + * @param snapRawValues snap raw value + * @param capDef capability + * @return return value associated or blank string + */ + protected String bitValue(String key, Map snapRawValues, final CapabilityDefinition capDef) { + // get the capability Values/MonitoringValues Map + // Look up the bit value for a specific key + if (snapRawValues.isEmpty()) { + logger.warn("No snapshot raw values provided. Corrupted data returned or bug"); + return ""; + } + Map> cachedBitKey = getSpecificCacheBitKey(capDef); + Map bitKey = this.getBitKey(key, capDef.getFeatureValuesRawData(), cachedBitKey); + if (bitKey.isEmpty()) { + logger.warn("BitKey {} not found in the Options feature values description capability. It's mostly a bug", + key); + return ""; + } + // Get the name of the option (Option1, Option2, etc) that contains the key (ex. LoadItem, RemoteStart) desired + String option = (String) bitKey.get("option"); + Object bitValueDef = snapRawValues.get(option); + if (bitValueDef == null) { + logger.warn("Value definition not found for the bitValue definition: {}. It's mostly a bug", option); + return ""; + } + String value = bitValueDef.toString(); + if (value.isEmpty()) { + return "0"; + } + + int bitValue = Integer.parseInt(value); + int startBit = (int) Objects.requireNonNull(bitKey.get("startbit"), "Not expected null here"); + int length = (int) bitKey.getOrDefault("length", 0); + int val = 0; + + for (int i = 0; i < length; i++) { + int bitIndex = (int) Math.pow(2, (startBit + i)); + int bit = (bitValue & bitIndex) != 0 ? 1 : 0; + val += bit * (int) Math.pow(2, i); + } + + return Integer.toString(val); + } + + protected synchronized Map> getSpecificCacheBitKey(CapabilityDefinition capDef) { + return Objects.requireNonNull( + modelsCachedBitKeyDefinitions.computeIfAbsent(capDef.getModelName(), k -> new HashMap<>())); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DevicePowerState.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DevicePowerState.java new file mode 100644 index 0000000000000..ae184d692e2d8 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DevicePowerState.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link DevicePowerState} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum DevicePowerState { + DV_POWER_ON(1), + DV_POWER_OFF(0), + DV_POWER_UNK(-1); + + private final int powerState; + + public double getValue() { + return powerState; + } + + DevicePowerState(int i) { + powerState = i; + } + + public static DevicePowerState statusOf(@Nullable Integer value) { + return switch (value == null ? -1 : value) { + case 0 -> DV_POWER_OFF; + case 1, 256, 257 -> DV_POWER_ON; + default -> DV_POWER_UNK; + }; + } + + public static double valueOf(DevicePowerState dps) { + return dps.powerState; + } + + /** + * Value of command (not state, but command to change the state of device) + * + * @return value of the command to reach the state + */ + public int commandValue() { + switch (this) { + case DV_POWER_ON: + return 257;// "@AC_MAIN_OPERATION_ALL_ON_W" + case DV_POWER_OFF: + return 0; // "@AC_MAIN_OPERATION_OFF_W" + default: + throw new IllegalArgumentException("Enum not accepted for command:" + this); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DeviceTypes.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DeviceTypes.java new file mode 100644 index 0000000000000..66550f3f384b9 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/DeviceTypes.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DeviceTypes} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum DeviceTypes { + AIR_CONDITIONER(401, "AC", "", "air-conditioner-401"), + HEAT_PUMP(401, "AC", "AWHP", "heatpump-401HP"), + WASHERDRYER_MACHINE(201, "WM", "", "washer-201"), + WASHER_TOWER(221, "WM", "", "washer-tower-221"), + DRYER(202, "DR", "Dryer", "dryer-202"), + DRYER_TOWER(222, "DR", "Dryer", "dryer-tower-222"), + FRIDGE(101, "REF", "Fridge", "fridge-101"), + DISH_WASHER(204, "DW", "DishWasher", "dishwasher-204"), + UNKNOWN(-1, "", "", ""); + + private final int deviceTypeId; + private final String deviceTypeAcron; + private final String deviceSubModel; + private final String thingTypeId; + + public String deviceTypeAcron() { + return deviceTypeAcron; + } + + public int deviceTypeId() { + return deviceTypeId; + } + + public String deviceSubModel() { + return deviceSubModel; + } + + public String thingTypeId() { + return thingTypeId; + } + + public static DeviceTypes fromDeviceTypeId(int deviceTypeId, String deviceCode) { + switch (deviceTypeId) { + case 401: + if ("AI05".equals(deviceCode)) { + return HEAT_PUMP; + } + return AIR_CONDITIONER; + case 201: + return WASHERDRYER_MACHINE; + case 221: + return WASHER_TOWER; + case 202: + return DRYER; + case 204: + return DISH_WASHER; + case 222: + return DRYER_TOWER; + case 101: + return FRIDGE; + default: + return UNKNOWN; + } + } + + public static DeviceTypes fromDeviceTypeAcron(String deviceTypeAcron, String modelType) { + return switch (deviceTypeAcron) { + case "AC" -> { + if ("AWHP".equals(modelType)) { + yield HEAT_PUMP; + } + yield AIR_CONDITIONER; + } + case "WM" -> { + if ("Dryer".equals(modelType)) { + yield DRYER; + } + yield WASHERDRYER_MACHINE; + } + case "REF" -> FRIDGE; + case "DW" -> DISH_WASHER; + default -> UNKNOWN; + }; + } + + DeviceTypes(int i, String n, String submodel, String thingTypeId) { + this.deviceTypeId = i; + this.deviceTypeAcron = n; + this.deviceSubModel = submodel; + this.thingTypeId = thingTypeId; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/FeatureDataType.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/FeatureDataType.java new file mode 100644 index 0000000000000..fb75a74724ee1 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/FeatureDataType.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link FeatureDataType} + * Feature is the values the device has to expose its sensor attributes + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum FeatureDataType { + ENUM, + RANGE, + BOOLEAN, + BIT, + REFERENCE, + UNDEF; + + public static FeatureDataType fromValue(String value) { + switch (value.toLowerCase()) { + case "enum": + return ENUM; + case "boolean": + return BOOLEAN; + case "bit": + return BIT; + case "range": + return RANGE; + case "reference": + return REFERENCE; + default: + return UNDEF; + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/FeatureDefinition.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/FeatureDefinition.java new file mode 100644 index 0000000000000..67f9f5255c4a7 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/FeatureDefinition.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link FeatureDefinition} defines the feature definitions extracted from the capability files in + * the MonitoringValue/Value session. All features are read-only by default. The factory must change-it if + * a specific one can be represented by a Writable Channel. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class FeatureDefinition { + public static final FeatureDefinition NULL_DEFINITION = new FeatureDefinition(); + String name = ""; + String channelId = ""; + String refChannelId = ""; + String label = ""; + Boolean readOnly = true; + FeatureDataType dataType = FeatureDataType.UNDEF; + Map valuesMapping = new HashMap<>(); + + /** + * Return the optional referenced channel Id. In some cases, the feature has a reference from another channel. + * In other words, in some cases, it copies or use value hold for other channels. + * + * @return the optional referenced field for this feature + */ + public String getRefChannelId() { + return refChannelId; + } + + /** + * Set the optional reference field for this channel In some cases, the feature has a reference from another + * channel. + * In other words, in some cases, it copies or use value hold for other channels. + * + * @param refChannelId the optional referenced field for this feature + */ + public void setRefChannelId(String refChannelId) { + this.refChannelId = refChannelId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public FeatureDataType getDataType() { + return dataType; + } + + public Boolean isReadOnly() { + return readOnly; + } + + public void setReadOnly(Boolean readOnly) { + this.readOnly = readOnly; + } + + public void setDataType(FeatureDataType dataType) { + this.dataType = dataType; + } + + public Map getValuesMapping() { + return valuesMapping; + } + + public void setValuesMapping(Map valuesMapping) { + this.valuesMapping = valuesMapping; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + FeatureDefinition that = (FeatureDefinition) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/LGAPIVerion.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/LGAPIVerion.java new file mode 100644 index 0000000000000..15af8042c223f --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/LGAPIVerion.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LGAPIVerion} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum LGAPIVerion { + V1_0(1.0), + V2_0(2.0), + UNDEF(0.0); + + private final double version; + + LGAPIVerion(double v) { + version = v; + } + + public double getValue() { + return version; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/LGDevice.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/LGDevice.java new file mode 100644 index 0000000000000..a0605abb5ff9f --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/LGDevice.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@link LGDevice} + * + * @author Nemer Daud - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@NonNullByDefault +public class LGDevice { + private String modelName = ""; + @JsonProperty("deviceType") + private int deviceTypeId; + private String deviceCode = ""; + private String alias = ""; + private String deviceId = ""; + private String platformType = ""; + private String modelJsonUri = ""; + private boolean online; + + public String getModelName() { + return modelName; + } + + @JsonIgnore + public DeviceTypes getDeviceType() { + return DeviceTypes.fromDeviceTypeId(deviceTypeId, deviceCode); + } + + public void setModelName(String modelName) { + this.modelName = modelName; + } + + public int getDeviceTypeId() { + return deviceTypeId; + } + + public void setDeviceTypeId(int deviceTypeId) { + this.deviceTypeId = deviceTypeId; + } + + public String getDeviceCode() { + return deviceCode; + } + + public void setDeviceCode(String deviceCode) { + this.deviceCode = deviceCode; + } + + public String getModelJsonUri() { + return modelJsonUri; + } + + public void setModelJsonUri(String modelJsonUri) { + this.modelJsonUri = modelJsonUri; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getPlatformType() { + return platformType; + } + + public void setPlatformType(String platformType) { + this.platformType = platformType; + } + + public boolean isOnline() { + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/ModelUtils.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/ModelUtils.java new file mode 100644 index 0000000000000..260003518a81b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/ModelUtils.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import static org.openhab.binding.lgthinq.lgservices.model.DeviceTypes.fromDeviceTypeAcron; + +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link ModelUtils} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class ModelUtils { + public static final ObjectMapper objectMapper = new ObjectMapper(); + + public static DeviceTypes getDeviceType(Map rootMap) { + Map infoMap = objectMapper.convertValue(rootMap.get("Info"), new TypeReference<>() { + }); + Objects.requireNonNull(infoMap, "Unexpected error. Info node not present in capability schema"); + String productType = infoMap.getOrDefault("productType", ""); + String modelType = infoMap.getOrDefault("modelType", ""); + Objects.requireNonNull(infoMap, "Unexpected error. ProductType attribute not present in capability schema"); + return fromDeviceTypeAcron(productType, modelType); + } + + public static DeviceTypes getDeviceType(JsonNode rootNode) { + Map mapper = objectMapper.convertValue(rootNode, new TypeReference<>() { + }); + return getDeviceType(mapper); + } + + public static LGAPIVerion discoveryAPIVersion(JsonNode rootNode) { + Map mapper = objectMapper.convertValue(rootNode, new TypeReference<>() { + }); + return discoveryAPIVersion(mapper); + } + + public static LGAPIVerion discoveryAPIVersion(Map rootMap) { + DeviceTypes type = getDeviceType(rootMap); + switch (type) { + case AIR_CONDITIONER: + case HEAT_PUMP: + Map valueNode = objectMapper.convertValue(rootMap.get("Value"), new TypeReference<>() { + }); + if (valueNode.containsKey("support.airState.opMode")) { + return LGAPIVerion.V2_0; + } else if (valueNode.containsKey("SupportOpMode")) { + return LGAPIVerion.V1_0; + } else { + throw new IllegalStateException( + "Unexpected error. Can't find key node attributes to determine ACCapability API version."); + } + + case WASHERDRYER_MACHINE: + case DRYER: + case FRIDGE: + if (rootMap.containsKey("Value")) { + return LGAPIVerion.V1_0; + } else if (rootMap.containsKey("MonitoringValue")) { + return LGAPIVerion.V2_0; + } else { + throw new IllegalStateException( + "Unexpected error. Can't find key node attributes to determine ACCapability API version."); + } + case DISH_WASHER: + return LGAPIVerion.V2_0; + default: + throw new IllegalStateException("Unexpected capability. The type " + type + " was not implemented yet"); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/MonitoringBinaryProtocol.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/MonitoringBinaryProtocol.java new file mode 100644 index 0000000000000..2a270c91f1a91 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/MonitoringBinaryProtocol.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@link MonitoringBinaryProtocol} + * + * @author Nemer Daud - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@NonNullByDefault +public class MonitoringBinaryProtocol { + @JsonProperty("startByte") + public int startByte; + @JsonProperty("length") + public int length; + @JsonProperty("value") + public String fieldName = ""; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/MonitoringResultFormat.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/MonitoringResultFormat.java new file mode 100644 index 0000000000000..69bde8877e6f6 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/MonitoringResultFormat.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MonitoringResultFormat} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum MonitoringResultFormat { + JSON_FORMAT(""), + BINARY_FORMAT("BINARY(BYTE)"), + UNKNOWN_FORMAT("UNKNOWN_FORMAT"); + + final String format; + + MonitoringResultFormat(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } + + public static MonitoringResultFormat getFormatOf(String formatValue) { + return switch (formatValue.toUpperCase()) { + case "BINARY(BYTE)" -> BINARY_FORMAT; + case "JSON" -> JSON_FORMAT; + default -> UNKNOWN_FORMAT; + }; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/ResultCodes.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/ResultCodes.java new file mode 100644 index 0000000000000..2ae0e48186ae9 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/ResultCodes.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link ResultCodes} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum ResultCodes { + DEVICE_OFFLINE("Device Offline", "0106"), + OK("Success", "0000", "0001"), + DEVICE_NOT_RESPONSE("Device Not Response", "0111", "0103", "0104", "0106"), + PORTAL_INTERWORKING_ERROR("Portal Internal Error", "0007"), + LOGIN_DUPLICATED("Login Duplicated", "0004"), + UPDATE_TERMS_NEEDED("Update Agreement Terms in LG App", "0110"), + LOGIN_FAILED("Login/Session Failed. Try to login and correct issues direct on LG Account Portal", "0102", "0114"), + BASE64_CODING_ERROR("Base64 Decoding/Encoding error", "9002", "9001"), + NOT_SUPPORTED_CONTROL("Command/Control/Service is not supported", "0005", "0012", "8001"), + CONTROL_ERROR("Error in device control", "0105"), + LG_SERVER_ERROR("LG Server Error/Invalid Request", "8101", "8102", "8103", "8104", "8105", "8106", "8107", "9003", + "9004", "9005", "9000", "8900", "0107"), + PAYLOAD_ERROR("Malformed or Wrong Payload", "9999"), + DUPLICATED_DATA("Duplicated Data/Alias", "0008", "0013"), + ACCESS_DENIED("Access Denied. Verify your account/password in LG Account Portal.", "9006", "0011", "0113"), + NOT_SUPPORTED_COUNTRY("Country not supported.", "8000"), + NETWORK_FAILED("Timeout/Network has failed.", "9020"), + LIMIT_EXCEEDED_ERROR("Limit has been exceeded", "0112"), + CUSTOMER_NUMBER_EXPIRED("Customer number has been expired", "0119"), + INVALID_CUSTOMER_DATA("Customer data is invalid or Data Doesn't exist.", "0010"), + GENERAL_FAILURE("General Failure", "0100"), + INVALID_CSR("Invalid CSR", "9010"), + INVALID_PAYLOAD("Invalid Body/Payload", "0002"), + INVALID_CUSTOMER_NUMBER("Invalid Customer Number", "0118", "120"), + INVALID_HEAD("Invalid Request Head", "0003"), + INVALID_PUSH_TOKEN("Invalid Push Token", "0301"), + INVALID_REQUEST("Invalid request", "0116"), + NOT_REGISTERED_SMART_CARE("Smart Care not registered", "0121"), + DEVICE_MISMATCH("Device/Group mismatch or device/model doesn't exist in your account.", "0115", "0006", "0009", + "0117", "0014"), + NO_INFORMATION_FOUND("No information found for the arguments", "109", "108"), + OTHER("Error processing request."), + UNKNOWN("UNKNOWN", ""); + + public static final Map OTHER_ERROR_CODE_RESPONSE = Map.ofEntries( + + Map.entry("0109", "NO_INFORMATION_DR"), Map.entry("0108", "NO_INFORMATION_SLEEP_MODE")); + + private final String description; + private final List codes; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public boolean containsResultCode(String code) { + return codes.contains(code); + } + + public String getDescription() { + return description; + } + + public static String getReasonResponse(String jsonResponse) { + + try { + JsonNode devicesResult = objectMapper.readValue(jsonResponse, new TypeReference<>() { + }); + String resultCode = devicesResult.path("resultCode").asText(); + return String.format("%s - %s", resultCode, fromCode(resultCode).description); + } catch (JsonProcessingException e) { + return ""; + } + } + + public List getCodes() { + return codes; + } + + ResultCodes(String description, String... codes) { + this.codes = Arrays.asList(codes); + this.description = description; + } + + public static ResultCodes fromCode(String code) { + return switch (code) { + case "0000", "0001" -> OK; + case "0002" -> INVALID_PAYLOAD; + case "0003" -> INVALID_HEAD; + case "0110" -> // Update Terms + UPDATE_TERMS_NEEDED; + case "0004" -> // Duplicated Login + LOGIN_DUPLICATED; // Not Logged in + case "0102", "0114" -> // Mismatch Login Session + LOGIN_FAILED; + case "0100" -> GENERAL_FAILURE; + case "0116" -> INVALID_REQUEST; + case "0108", "0109" -> NO_INFORMATION_FOUND; + case "0115", "0006", "0009", "0117", "0014", "0101" -> DEVICE_MISMATCH; + case "0010" -> INVALID_CUSTOMER_DATA; + case "0112" -> LIMIT_EXCEEDED_ERROR; + case "0118", "0120" -> INVALID_CUSTOMER_NUMBER; + case "0121" -> NOT_REGISTERED_SMART_CARE; + case "0007" -> PORTAL_INTERWORKING_ERROR; + case "0008", "0013" -> DUPLICATED_DATA; + case "0005", "0012", "8001" -> NOT_SUPPORTED_CONTROL; + case "0111", "0103", "0104", "0106" -> DEVICE_NOT_RESPONSE; + case "0105" -> CONTROL_ERROR; + case "9001", "9002" -> BASE64_CODING_ERROR; + case "0107", "8101", "8102", "8203", "8204", "8205", "8206", "8207", "8900", "9000", "9003", "9004", + "9005" -> + LG_SERVER_ERROR; + case "9999" -> PAYLOAD_ERROR; + case "9006", "0011", "0113" -> ACCESS_DENIED; + case "9010" -> INVALID_CSR; + case "0301" -> INVALID_PUSH_TOKEN; + default -> { + if (OTHER_ERROR_CODE_RESPONSE.containsKey(code)) { + yield OTHER; + } + yield UNKNOWN; + } + }; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotBuilder.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotBuilder.java new file mode 100644 index 0000000000000..de395d16bcc12 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotBuilder.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; + +/** + * The {@link SnapshotBuilder} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface SnapshotBuilder { + S createFromBinary(String binaryData, List prot, CapabilityDefinition capDef) + throws LGThinqUnmarshallException, LGThinqApiException; + + S createFromJson(String snapshotDataJson, DeviceTypes deviceType, CapabilityDefinition capDef) + throws LGThinqUnmarshallException, LGThinqApiException; + + S createFromJson(Map deviceSettings, CapabilityDefinition capDef) throws LGThinqApiException; +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotBuilderFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotBuilderFactory.java new file mode 100644 index 0000000000000..201f348c22392 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotBuilderFactory.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACSnapshotBuilder; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher.DishWasherSnapshotBuilder; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.fridge.FridgeSnapshotBuilder; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerSnapshotBuilder; + +/** + * The {@link SnapshotBuilderFactory} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class SnapshotBuilderFactory { + private final Map, SnapshotBuilder> internalBuilders = new HashMap<>(); + + private static final SnapshotBuilderFactory instance; + static { + instance = new SnapshotBuilderFactory(); + } + + private SnapshotBuilderFactory() { + } + + public static SnapshotBuilderFactory getInstance() { + return instance; + } + + public SnapshotBuilder getBuilder(Class snapDef) { + SnapshotBuilder result = internalBuilders.get(snapDef); + if (result == null) { + if (snapDef.equals(WasherDryerSnapshot.class)) { + result = new WasherDryerSnapshotBuilder(); + } else if (snapDef.equals(ACCanonicalSnapshot.class)) { + result = new ACSnapshotBuilder(); + } else if (snapDef.equals(FridgeCanonicalSnapshot.class)) { + result = new FridgeSnapshotBuilder(); + } else if (snapDef.equals(DishWasherSnapshot.class)) { + result = new DishWasherSnapshotBuilder(); + } else { + throw new IllegalStateException( + "Snapshot definition " + snapDef + " not supported by this Factory. It most likely a bug"); + } + internalBuilders.put(snapDef, result); + } + return result; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotDefinition.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotDefinition.java new file mode 100644 index 0000000000000..9ed74e876a909 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/SnapshotDefinition.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SnapshotDefinition} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface SnapshotDefinition { + DevicePowerState getPowerStatus(); + + void setPowerStatus(DevicePowerState value); + + boolean isOnline(); + + void setOnline(boolean online); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCanonicalSnapshot.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCanonicalSnapshot.java new file mode 100644 index 0000000000000..8d45feeeb20ef --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCanonicalSnapshot.java @@ -0,0 +1,298 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@link ACCanonicalSnapshot} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@JsonIgnoreProperties(ignoreUnknown = true) +public class ACCanonicalSnapshot extends AbstractSnapshotDefinition { + + // ============ FOR HEAT PUMP ONLY =============== + private double hpWaterTempCoolMin; + private double hpWaterTempCoolMax; + private double hpWaterTempHeatMin; + private double hpWaterTempHeatMax; + private double hpAirTempCoolMin; + private double hpAirTempCoolMax; + private double hpAirTempHeatMin; + private double hpAirTempHeatMax; + private double hpAirWaterTempSwitch = -1; + // =============================================== + + private int airWindStrength; + private double targetTemperature; + private double currentTemperature; + private double airCleanMode; + private double coolJetMode; + private double autoDryMode; + private double energySavingMode; + private double stepUpDownMode; + private double stepLeftRightMode; + + private int operationMode; + @Nullable + private Integer operation; + @JsonIgnore + private boolean online; + + private double energyConsumption; + + @JsonIgnore + public DevicePowerState getPowerStatus() { + return DevicePowerState.statusOf(operation); + } + + @JsonIgnore + public void setPowerStatus(DevicePowerState value) { + operation = (int) value.getValue(); + } + + @JsonIgnore + public ACFanSpeed getAcFanSpeed() { + return ACFanSpeed.statusOf(airWindStrength); + } + + @JsonProperty("airState.windStrength") + @JsonAlias("WindStrength") + public Integer getAirWindStrength() { + return airWindStrength; + } + + @JsonProperty("airState.wMode.jet") + @JsonAlias("Jet") + public Double getCoolJetMode() { + return coolJetMode; + } + + @JsonProperty("airState.wMode.airClean") + @JsonAlias("AirClean") + public Double getAirCleanMode() { + return airCleanMode; + } + + @JsonProperty("airState.miscFuncState.autoDry") + @JsonAlias("AutoDry") + public Double getAutoDryMode() { + return autoDryMode; + } + + @JsonProperty("airState.powerSave.basic") + @JsonAlias("PowerSave") + public Double getEnergySavingMode() { + return energySavingMode; + } + + public void setAirCleanMode(double airCleanMode) { + this.airCleanMode = airCleanMode; + } + + public void setAutoDryMode(double autoDryMode) { + this.autoDryMode = autoDryMode; + } + + public void setEnergySavingMode(double energySavingMode) { + this.energySavingMode = energySavingMode; + } + + public void setCoolJetMode(Double coolJetMode) { + this.coolJetMode = coolJetMode; + } + + public void setAirWindStrength(Integer airWindStrength) { + this.airWindStrength = airWindStrength; + } + + @JsonProperty("airState.energy.onCurrent") + public double getEnergyConsumption() { + return energyConsumption; + } + + public void setEnergyConsumption(double energyConsumption) { + this.energyConsumption = energyConsumption; + } + + @JsonProperty("airState.tempState.target") + @JsonAlias("TempCfg") + public Double getTargetTemperature() { + return targetTemperature; + } + + public void setTargetTemperature(Double targetTemperature) { + this.targetTemperature = targetTemperature; + } + + @JsonProperty("airState.tempState.current") + @JsonAlias("TempCur") + public Double getCurrentTemperature() { + return currentTemperature; + } + + public void setCurrentTemperature(Double currentTemperature) { + this.currentTemperature = currentTemperature; + } + + @JsonProperty("airState.opMode") + @JsonAlias("OpMode") + public Integer getOperationMode() { + return operationMode; + } + + public void setOperationMode(Integer operationMode) { + this.operationMode = operationMode; + } + + @Nullable + @JsonProperty("airState.operation") + @JsonAlias("Operation") + public Integer getOperation() { + return operation; + } + + public void setOperation(Integer operation) { + this.operation = operation; + } + + @JsonProperty("airState.wDir.vStep") + @JsonAlias("WDirVStep") + public double getStepUpDownMode() { + return stepUpDownMode; + } + + public void setStepUpDownMode(double stepUpDownMode) { + this.stepUpDownMode = stepUpDownMode; + } + + @JsonProperty("airState.wDir.hStep") + @JsonAlias("WDirHStep") + public double getStepLeftRightMode() { + return stepLeftRightMode; + } + + public void setStepLeftRightMode(double stepLeftRightMode) { + this.stepLeftRightMode = stepLeftRightMode; + } + + @JsonIgnore + public boolean isOnline() { + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } + + // ==================== For HP only + @JsonProperty("airState.tempState.waterTempCoolMin") + public double getHpWaterTempCoolMin() { + return hpWaterTempCoolMin; + } + + public void setHpWaterTempCoolMin(double hpWaterTempCoolMin) { + this.hpWaterTempCoolMin = hpWaterTempCoolMin; + } + + @JsonProperty("airState.tempState.waterTempCoolMax") + public double getHpWaterTempCoolMax() { + return hpWaterTempCoolMax; + } + + public void setHpWaterTempCoolMax(double hpWaterTempCoolMax) { + this.hpWaterTempCoolMax = hpWaterTempCoolMax; + } + + @JsonProperty("airState.tempState.waterTempHeatMin") + public double getHpWaterTempHeatMin() { + return hpWaterTempHeatMin; + } + + public void setHpWaterTempHeatMin(double hpWaterTempHeatMin) { + this.hpWaterTempHeatMin = hpWaterTempHeatMin; + } + + @JsonProperty("airState.tempState.waterTempHeatMax") + public double getHpWaterTempHeatMax() { + return hpWaterTempHeatMax; + } + + public void setHpWaterTempHeatMax(double hpWaterTempHeatMax) { + this.hpWaterTempHeatMax = hpWaterTempHeatMax; + } + + @JsonProperty("airState.tempState.airTempCoolMin") + public double getHpAirTempCoolMin() { + return hpAirTempCoolMin; + } + + public void setHpAirTempCoolMin(double hpAirTempCoolMin) { + this.hpAirTempCoolMin = hpAirTempCoolMin; + } + + @JsonProperty("airState.tempState.airTempCoolMax") + public double getHpAirTempCoolMax() { + return hpAirTempCoolMax; + } + + public void setHpAirTempCoolMax(double hpAirTempCoolMax) { + this.hpAirTempCoolMax = hpAirTempCoolMax; + } + + @JsonProperty("airState.tempState.airTempHeatMin") + public double getHpAirTempHeatMin() { + return hpAirTempHeatMin; + } + + public void setHpAirTempHeatMin(double hpAirTempHeatMin) { + this.hpAirTempHeatMin = hpAirTempHeatMin; + } + + @JsonProperty("airState.tempState.airTempHeatMax") + public double getHpAirTempHeatMax() { + return hpAirTempHeatMax; + } + + public void setHpAirTempHeatMax(double hpAirTempHeatMax) { + this.hpAirTempHeatMax = hpAirTempHeatMax; + } + + @JsonProperty("airState.miscFuncState.awhpTempSwitch") + public double getHpAirWaterTempSwitch() { + return hpAirWaterTempSwitch; + } + + public void setHpAirWaterTempSwitch(double hpAirWaterTempSwitch) { + this.hpAirWaterTempSwitch = hpAirWaterTempSwitch; + } + // =================================== + + @Override + public String toString() { + return "ACSnapShot{" + "airWindStrength=" + airWindStrength + ", targetTemperature=" + targetTemperature + + ", currentTemperature=" + currentTemperature + ", operationMode=" + operationMode + ", operation=" + + operation + ", acPowerStatus=" + getPowerStatus() + ", acFanSpeed=" + getAcFanSpeed() + ", acOpMode=" + + ", online=" + isOnline() + " }"; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapability.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapability.java new file mode 100644 index 0000000000000..85581e40cad44 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapability.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapability; + +/** + * The {@link ACCapability} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class ACCapability extends AbstractCapability { + + private Map opMod = Collections.emptyMap(); + private Map fanSpeed = Collections.emptyMap(); + private Map stepUpDown = Collections.emptyMap(); + private Map stepLeftRight = Collections.emptyMap(); + private boolean isJetModeAvailable; + private boolean isStepUpDownAvailable; + private boolean isStepLeftRightAvailable; + private boolean isEnergyMonitorAvailable; + private boolean isFilterMonitorAvailable; + private boolean isAutoDryModeAvailable; + private boolean isEnergySavingAvailable; + private boolean isAirCleanAvailable; + private String coolJetModeCommandOn = ""; + private String coolJetModeCommandOff = ""; + private String autoDryModeCommandOn = ""; + private String autoDryModeCommandOff = ""; + + private String energySavingModeCommandOn = ""; + private String energySavingModeCommandOff = ""; + + private String airCleanModeCommandOn = ""; + private String airCleanModeCommandOff = ""; + + public Map getStepLeftRight() { + return stepLeftRight; + } + + public void setStepLeftRight(Map stepLeftRight) { + this.stepLeftRight = stepLeftRight; + } + + public Map getStepUpDown() { + return stepUpDown; + } + + public void setStepUpDown(Map stepUpDown) { + this.stepUpDown = stepUpDown; + } + + public String getCoolJetModeCommandOff() { + return coolJetModeCommandOff; + } + + public void setCoolJetModeCommandOff(String coolJetModeCommandOff) { + this.coolJetModeCommandOff = coolJetModeCommandOff; + } + + public String getCoolJetModeCommandOn() { + return coolJetModeCommandOn; + } + + public void setCoolJetModeCommandOn(String coolJetModeCommandOn) { + this.coolJetModeCommandOn = coolJetModeCommandOn; + } + + public boolean isStepUpDownAvailable() { + return isStepUpDownAvailable; + } + + public void setStepUpDownAvailable(boolean stepUpDownAvailable) { + isStepUpDownAvailable = stepUpDownAvailable; + } + + public boolean isStepLeftRightAvailable() { + return isStepLeftRightAvailable; + } + + public void setStepLeftRightAvailable(boolean stepLeftRightAvailable) { + isStepLeftRightAvailable = stepLeftRightAvailable; + } + + public Map getOpMode() { + return opMod; + } + + public void setOpMod(Map opMod) { + this.opMod = opMod; + } + + public Map getFanSpeed() { + return fanSpeed; + } + + public void setFanSpeed(Map fanSpeed) { + this.fanSpeed = fanSpeed; + } + + public void setJetModeAvailable(boolean jetModeAvailable) { + this.isJetModeAvailable = jetModeAvailable; + } + + public boolean isAutoDryModeAvailable() { + return isAutoDryModeAvailable; + } + + public void setAutoDryModeAvailable(boolean autoDryModeAvailable) { + isAutoDryModeAvailable = autoDryModeAvailable; + } + + public boolean isEnergySavingAvailable() { + return isEnergySavingAvailable; + } + + public void setEnergySavingAvailable(boolean energySavingAvailable) { + isEnergySavingAvailable = energySavingAvailable; + } + + public boolean isAirCleanAvailable() { + return isAirCleanAvailable; + } + + public void setAirCleanAvailable(boolean airCleanAvailable) { + isAirCleanAvailable = airCleanAvailable; + } + + public boolean isJetModeAvailable() { + return this.isJetModeAvailable; + } + + public String getAutoDryModeCommandOn() { + return autoDryModeCommandOn; + } + + public void setAutoDryModeCommandOn(String autoDryModeCommandOn) { + this.autoDryModeCommandOn = autoDryModeCommandOn; + } + + public String getAutoDryModeCommandOff() { + return autoDryModeCommandOff; + } + + public void setAutoDryModeCommandOff(String autoDryModeCommandOff) { + this.autoDryModeCommandOff = autoDryModeCommandOff; + } + + public String getEnergySavingModeCommandOn() { + return energySavingModeCommandOn; + } + + public void setEnergySavingModeCommandOn(String energySavingModeCommandOn) { + this.energySavingModeCommandOn = energySavingModeCommandOn; + } + + public String getEnergySavingModeCommandOff() { + return energySavingModeCommandOff; + } + + public void setEnergySavingModeCommandOff(String energySavingModeCommandOff) { + this.energySavingModeCommandOff = energySavingModeCommandOff; + } + + public String getAirCleanModeCommandOn() { + return airCleanModeCommandOn; + } + + public void setAirCleanModeCommandOn(String airCleanModeCommandOn) { + this.airCleanModeCommandOn = airCleanModeCommandOn; + } + + public String getAirCleanModeCommandOff() { + return airCleanModeCommandOff; + } + + public void setAirCleanModeCommandOff(String airCleanModeCommandOff) { + this.airCleanModeCommandOff = airCleanModeCommandOff; + } + + public boolean isEnergyMonitorAvailable() { + return isEnergyMonitorAvailable; + } + + public void setEnergyMonitorAvailable(boolean energyMonitorAvailable) { + isEnergyMonitorAvailable = energyMonitorAvailable; + } + + public boolean isFilterMonitorAvailable() { + return isFilterMonitorAvailable; + } + + public void setFilterMonitorAvailable(boolean filterMonitorAvailable) { + isFilterMonitorAvailable = filterMonitorAvailable; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapabilityFactoryV1.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapabilityFactoryV1.java new file mode 100644 index 0000000000000..d689c23b146af --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapabilityFactoryV1.java @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link ACCapabilityFactoryV1} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class ACCapabilityFactoryV1 extends AbstractACCapabilityFactory { + + @Override + protected List getSupportedAPIVersions() { + return List.of(LGAPIVerion.V1_0); + } + + @Override + protected Map getCommandsDefinition(JsonNode rootNode) { + return Collections.emptyMap(); + } + + @Override + protected Map extractFeatureOptions(JsonNode optionsNode) { + Map options = new HashMap<>(); + optionsNode.fields().forEachRemaining(o -> { + options.put(o.getKey(), o.getValue().asText()); + }); + return options; + } + + @Override + public ACCapability create(JsonNode rootNode) throws LGThinqException { + ACCapability cap = super.create(rootNode); + // set energy and filter availability (extended info) + cap.setEnergyMonitorAvailable( + !rootNode.path("ControlWifi").path("action").path("GetInOutInstantPower").isMissingNode()); + cap.setFilterMonitorAvailable( + !rootNode.path("ControlWifi").path("action").path("GetFilterUse").isMissingNode()); + return cap; + } + + @Override + protected String getDataTypeFeatureNodeName() { + return "type"; + } + + @Override + protected String getOpModeNodeName() { + return "OpMode"; + } + + @Override + protected String getFanSpeedNodeName() { + return "WindStrength"; + } + + @Override + protected String getSupOpModeNodeName() { + return "SupportOpMode"; + } + + @Override + protected String getSupFanSpeedNodeName() { + return "SupportWindStrength"; + } + + @Override + protected String getJetModeNodeName() { + return "Jet"; + } + + @Override + protected String getStepUpDownNodeName() { + return "WDirVStep"; + } + + @Override + protected String getStepLeftRightNodeName() { + return "WDirHStep"; + } + + @Override + protected String getSupSubRacModeNodeName() { + return "SupportRACSubMode"; + } + + @Override + protected String getSupRacModeNodeName() { + return "SupportRACMode"; + } + + @Override + protected String getAutoDryStateNodeName() { + return "AutoDry"; + } + + @Override + protected String getAirCleanStateNodeName() { + return "AirClean"; + } + + @Override + protected String getOptionsMapNodeName() { + return "option"; + } + + @Override + protected String getValuesNodeName() { + return "Value"; + } + + @Override + protected String getHpAirWaterSwitchNodeName() { + throw new UnsupportedOperationException("Heat Pump Thinq V1 not implemented yet! Ignoring node"); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapabilityFactoryV2.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapabilityFactoryV2.java new file mode 100644 index 0000000000000..4c64ab9580f0d --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACCapabilityFactoryV2.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link ACCapabilityFactoryV2} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class ACCapabilityFactoryV2 extends AbstractACCapabilityFactory { + + @Override + protected List getSupportedAPIVersions() { + return List.of(LGAPIVerion.V2_0); + } + + @Override + protected Map getCommandsDefinition(JsonNode rootNode) { + Map result = new HashMap<>(); + JsonNode controlDeviceNode = rootNode.path("ControlDevice"); + if (controlDeviceNode.isArray()) { + controlDeviceNode.forEach(c -> { + String ctrlKey = c.path("ctrlKey").asText(); + // commands variations are described separated by pipe "|" + String[] commands = c.path("command").asText().split("\\|"); + String dataValues = c.path("dataValue").asText(); + for (String cmd : commands) { + CommandDefinition cd = new CommandDefinition(); + cd.setCommand(cmd); + cd.setCmdOptValue(dataValues.replaceAll("[{%}]", "")); + cd.setRawCommand(c.toPrettyString()); + result.put(ctrlKey, cd); + } + }); + } + return result; + } + + @Override + protected String getOpModeNodeName() { + return "airState.opMode"; + } + + @Override + protected String getFanSpeedNodeName() { + return "airState.windStrength"; + } + + @Override + protected String getSupOpModeNodeName() { + return "support.airState.opMode"; + } + + @Override + protected String getSupFanSpeedNodeName() { + return "support.airState.windStrength"; + } + + @Override + protected String getJetModeNodeName() { + return "airState.wMode.jet"; + } + + @Override + protected String getStepUpDownNodeName() { + return "airState.wDir.vStep"; + } + + @Override + protected String getStepLeftRightNodeName() { + return "airState.wDir.hStep"; + } + + @Override + protected String getSupSubRacModeNodeName() { + return "support.racSubMode"; + } + + @Override + protected String getSupRacModeNodeName() { + return "support.racMode"; + } + + @Override + protected String getAutoDryStateNodeName() { + return "airState.miscFuncState.autoDry"; + } + + @Override + protected String getAirCleanStateNodeName() { + return "airState.wMode.airClean"; + } + + @Override + protected String getOptionsMapNodeName() { + return "value_mapping"; + } + + @Override + protected String getValuesNodeName() { + return "Value"; + } + + @Override + protected String getDataTypeFeatureNodeName() { + return "dataType"; + } + + @Override + protected Map extractFeatureOptions(JsonNode optionsNode) { + Map options = new HashMap<>(); + optionsNode.fields().forEachRemaining(o -> { + options.put(o.getKey(), o.getValue().path("label").asText()); + }); + return options; + } + + @Override + protected String getHpAirWaterSwitchNodeName() { + return "airState.miscFuncState.awhpTempSwitch"; + } + + @Override + public ACCapability create(JsonNode rootNode) throws LGThinqException { + ACCapability cap = super.create(rootNode); + Map cmd = getCommandsDefinition(rootNode); + // set energy and filter availability (extended info) + cap.setEnergyMonitorAvailable(cmd.containsKey("energyStateCtrl")); + cap.setFilterMonitorAvailable(cmd.containsKey("filterMngStateCtrl")); + return cap; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACFanSpeed.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACFanSpeed.java new file mode 100644 index 0000000000000..7a0188cc92962 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACFanSpeed.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ACCanonicalSnapshot} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum ACFanSpeed { + F1(2.0), + F2(3.0), + F3(4.0), + F4(5.0), + F5(6.0), + F_AUTO(8.0), + F_UNK(-1); + + ACFanSpeed(double v) { + } + + public static ACFanSpeed statusOf(double value) { + return switch ((int) value) { + case 2 -> F1; + case 3 -> F2; + case 4 -> F3; + case 5 -> F4; + case 6 -> F5; + case 8 -> F_AUTO; + default -> F_UNK; + }; + } + + /** + * "0": "@AC_MAIN_WIND_STRENGTH_SLOW_W", + * "1": "@AC_MAIN_WIND_STRENGTH_SLOW_LOW_W", + * "2": "@AC_MAIN_WIND_STRENGTH_LOW_W", + * "3": "@AC_MAIN_WIND_STRENGTH_LOW_MID_W", + * "4": "@AC_MAIN_WIND_STRENGTH_MID_W", + * "5": "@AC_MAIN_WIND_STRENGTH_MID_HIGH_W", + * "6": "@AC_MAIN_WIND_STRENGTH_HIGH_W", + * "7": "@AC_MAIN_WIND_STRENGTH_POWER_W", + * "8": "@AC_MAIN_WIND_STRENGTH_NATURE_W", + */ + /** + * Value of command (not state, but command to change the state of device) + * + * @return value of the command to reach the state + */ + public int commandValue() { + switch (this) { + case F1: + return 2; + case F2: + return 3; + case F3: + return 4; + case F4: + return 5; + case F5: + return 6; + case F_AUTO: + return 8; + default: + throw new IllegalArgumentException("Enum not accepted for command:" + this); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACOpMode.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACOpMode.java new file mode 100644 index 0000000000000..4d356153ace4e --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACOpMode.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ACCanonicalSnapshot} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum ACOpMode { + COOL(0), + DRY(1), + FAN(2), + AI(3), + HEAT(4), + AIRCLEAN(5), + ENSAV(8), + OP_UNK(-1); + + private final int opMode; + + ACOpMode(int v) { + this.opMode = v; + } + + public static ACOpMode statusOf(int value) { + switch (value) { + case 0: + return COOL; + case 1: + return DRY; + case 2: + return FAN; + case 3: + return AI; + case 4: + return HEAT; + case 5: + return AIRCLEAN; + case 8: + return ENSAV; + default: + return OP_UNK; + } + } + + public int getValue() { + return this.opMode; + } + + /** + * Value of command (not state, but command to change the state of device) + * + * @return value of the command to reach the state + */ + public int commandValue() { + switch (this) { + case COOL: + return 0;// "@AC_MAIN_OPERATION_MODE_COOL_W" + case DRY: + return 1; // "@AC_MAIN_OPERATION_MODE_DRY_W" + case FAN: + return 2; // "@AC_MAIN_OPERATION_MODE_FAN_W" + case AI: + return 3; // "@AC_MAIN_OPERATION_MODE_AI_W" + case HEAT: + return 4; // "@AC_MAIN_OPERATION_MODE_HEAT_W" + case AIRCLEAN: + return 5; // "@AC_MAIN_OPERATION_MODE_AIRCLEAN_W" + case ENSAV: + return 8; // "AC_MAIN_OPERATION_MODE_ENERGY_SAVING_W" + default: + throw new IllegalArgumentException("Enum not accepted for command:" + this); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACSnapshotBuilder.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACSnapshotBuilder.java new file mode 100644 index 0000000000000..c5d203a42a466 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACSnapshotBuilder.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DefaultSnapshotBuilder; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringBinaryProtocol; + +/** + * The {@link ACSnapshotBuilder} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class ACSnapshotBuilder extends DefaultSnapshotBuilder { + public ACSnapshotBuilder() { + super(ACCanonicalSnapshot.class); + } + + @Override + public ACCanonicalSnapshot createFromBinary(String binaryData, List prot, + CapabilityDefinition capDef) throws LGThinqUnmarshallException, LGThinqApiException { + return super.createFromBinary(binaryData, prot, capDef); + } + + @Override + protected ACCanonicalSnapshot getSnapshot(Map snapMap, CapabilityDefinition capDef) { + ACCanonicalSnapshot snap; + switch (capDef.getDeviceType()) { + case AIR_CONDITIONER: + case HEAT_PUMP: + snap = objectMapper.convertValue(snapMap, snapClass); + snap.setRawData(snapMap); + return snap; + default: + throw new IllegalStateException("Snapshot for device type " + capDef.getDeviceType() + + " not supported for this builder. It most likely a bug"); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACTargetTmp.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACTargetTmp.java new file mode 100644 index 0000000000000..26f0c9acdfb31 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ACTargetTmp.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ACTargetTmp} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum ACTargetTmp { + _17(17.0), + _18(18.0), + _19(19.0), + _20(20.0), + _21(21.0), + _22(22.0), + _23(23.0), + _24(24.0), + _25(25.0), + _26(26.0), + _27(27.0), + _28(28.0), + _29(29.0), + _30(30.0), + UNK(-1); + + private final double targetTmp; + + ACTargetTmp(double v) { + this.targetTmp = v; + } + + public static ACTargetTmp statusOf(double value) { + switch ((int) value) { + case 17: + return _17; + case 18: + return _18; + case 19: + return _19; + case 20: + return _20; + case 21: + return _21; + case 22: + return _22; + case 23: + return _23; + case 24: + return _24; + case 25: + return _25; + case 26: + return _26; + case 27: + return _27; + case 28: + return _28; + case 29: + return _29; + case 30: + return _30; + default: + return UNK; + } + } + + public double getValue() { + return this.targetTmp; + } + + /** + * Value of command (not state, but command to change the state of device) + * + * @return value of the command to reach the state + */ + public int commandValue() { + return (int) this.targetTmp; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/AbstractACCapabilityFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/AbstractACCapabilityFactory.java new file mode 100644 index 0000000000000..3b14768bd240a --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/AbstractACCapabilityFactory.java @@ -0,0 +1,306 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_AIRCLEAN; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_AIR_CLEAN_COMMAND_OFF; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_AIR_CLEAN_COMMAND_ON; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_AUTODRY; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_COMMAND_OFF; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_COMMAND_ON; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_COOL_JET; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_ENERGYSAVING; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_SUB_MODE_COOL_JET; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_SUB_MODE_STEP_LEFT_RIGHT; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_AC_SUB_MODE_STEP_UP_DOWN; +import static org.openhab.binding.lgthinq.lgservices.model.DeviceTypes.HEAT_PUMP; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapabilityFactory; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDataType; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringResultFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link AbstractACCapabilityFactory} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractACCapabilityFactory extends AbstractCapabilityFactory { + private final Logger logger = LoggerFactory.getLogger(AbstractACCapabilityFactory.class); + + @Override + public final List getSupportedDeviceTypes() { + return List.of(DeviceTypes.AIR_CONDITIONER, HEAT_PUMP); + } + + protected abstract Map extractFeatureOptions(JsonNode optionsNode); + + @Override + protected FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode, + @Nullable String targetChannelId, @Nullable String refChannelId) { + JsonNode featureNode = featuresNode.path(featureName); + if (featureNode.isMissingNode()) { + return FeatureDefinition.NULL_DEFINITION; + } + FeatureDefinition fd = new FeatureDefinition(); + fd.setName(featureName); + fd.setDataType(FeatureDataType.fromValue(featureNode.path(getDataTypeFeatureNodeName()).asText())); + JsonNode options = featureNode.path(getOptionsMapNodeName()); + if (options.isMissingNode()) { + return FeatureDefinition.NULL_DEFINITION; + } + fd.setValuesMapping(extractFeatureOptions(options)); + return fd; + } + + protected abstract String getDataTypeFeatureNodeName(); + + private List extractValueOptions(JsonNode optionsNode) throws LGThinqApiException { + if (optionsNode.isMissingNode()) { + throw new LGThinqApiException("Error extracting options supported by the device"); + } else { + List values = new ArrayList<>(); + optionsNode.fields().forEachRemaining(e -> { + values.add(e.getValue().asText()); + }); + return values; + } + } + + private Map extractOptions(JsonNode optionsNode, boolean invertKeyValue) { + if (optionsNode.isMissingNode()) { + logger.warn("Error extracting options supported by the device"); + return Collections.emptyMap(); + } else { + Map modes = new HashMap(); + optionsNode.fields().forEachRemaining(e -> { + if (invertKeyValue) { + modes.put(e.getValue().asText(), e.getKey()); + } else { + modes.put(e.getKey(), e.getValue().asText()); + } + }); + return modes; + } + } + + @Override + public ACCapability create(JsonNode rootNode) throws LGThinqException { + ACCapability acCap = super.create(rootNode); + + JsonNode valuesNode = rootNode.path(getValuesNodeName()); + if (valuesNode.isMissingNode()) { + throw new LGThinqApiException("Error extracting capabilities supported by the device"); + } + // supported operation modes + Map allOpModes = extractOptions( + valuesNode.path(getOpModeNodeName()).path(getOptionsMapNodeName()), true); + Map allFanSpeeds = extractOptions( + valuesNode.path(getFanSpeedNodeName()).path(getOptionsMapNodeName()), true); + + List supOpModeValues = extractValueOptions( + valuesNode.path(getSupOpModeNodeName()).path(getOptionsMapNodeName())); + List supFanSpeedValues = extractValueOptions( + valuesNode.path(getSupFanSpeedNodeName()).path(getOptionsMapNodeName())); + supOpModeValues.remove("@NON"); + supOpModeValues.remove("@NON"); + // find correct operation IDs + Map opModes = new HashMap<>(supOpModeValues.size()); + supOpModeValues.forEach(v -> { + // discovery ID of the operation + String key = allOpModes.get(v); + if (key != null) { + opModes.put(key, v); + } + }); + acCap.setOpMod(opModes); + + Map fanSpeeds = new HashMap<>(supFanSpeedValues.size()); + supFanSpeedValues.forEach(v -> { + // discovery ID of the fan speed + String key = allFanSpeeds.get(v); + if (key != null) { + fanSpeeds.put(key, v); + } + }); + acCap.setFanSpeed(fanSpeeds); + + // ===== get supported extra modes + + JsonNode supRacSubModeOps = valuesNode.path(getSupSubRacModeNodeName()).path(getOptionsMapNodeName()); + if (!supRacSubModeOps.isMissingNode()) { + supRacSubModeOps.fields().forEachRemaining(f -> { + if (CAP_AC_SUB_MODE_COOL_JET.equals(f.getValue().asText())) { + acCap.setJetModeAvailable(true); + } + if (CAP_AC_SUB_MODE_STEP_UP_DOWN.equals(f.getValue().asText())) { + acCap.setStepUpDownAvailable(true); + } + if (CAP_AC_SUB_MODE_STEP_LEFT_RIGHT.equals(f.getValue().asText())) { + acCap.setStepLeftRightAvailable(true); + } + }); + } + + // set Cool jetMode supportability + if (acCap.isJetModeAvailable()) { + JsonNode jetModeOps = valuesNode.path(getJetModeNodeName()).path(getOptionsMapNodeName()); + if (!jetModeOps.isMissingNode()) { + jetModeOps.fields().forEachRemaining(j -> { + String value = j.getValue().asText(); + if (CAP_AC_COOL_JET.containsKey(value)) { + acCap.setCoolJetModeCommandOn(j.getKey()); + } else if (CAP_AC_COMMAND_OFF.equals(value)) { + acCap.setCoolJetModeCommandOff(j.getKey()); + } + }); + } + } + // ============== Collect Wind Direction (Up-Down, Left-Right) if supported ================== + if (acCap.isStepUpDownAvailable()) { + Map stepUpDownValueMap = extractOptions( + valuesNode.path(getStepUpDownNodeName()).path(getOptionsMapNodeName()), false); + // remove options who value doesn't start with @, that indicates for this feature that is not supported + stepUpDownValueMap.values().removeIf(v -> !v.startsWith("@")); + acCap.setStepUpDown(stepUpDownValueMap); + } + + if (acCap.isStepLeftRightAvailable()) { + Map stepLeftRightValueMap = extractOptions( + valuesNode.path(getStepLeftRightNodeName()).path(getOptionsMapNodeName()), false); + // remove options who value doesn't start with @, that indicates for this feature that is not supported + stepLeftRightValueMap.values().removeIf(v -> !v.startsWith("@")); + acCap.setStepLeftRight(stepLeftRightValueMap); + } + // =================================================== // + + // get Supported RAC Mode + JsonNode supRACModeOps = valuesNode.path(getSupRacModeNodeName()).path(getOptionsMapNodeName()); + + if (!supRACModeOps.isMissingNode()) { + supRACModeOps.fields().forEachRemaining(r -> { + String racOpValue = r.getValue().asText(); + switch (racOpValue) { + case CAP_AC_AUTODRY: + Map dryStates = extractOptions( + valuesNode.path(getAutoDryStateNodeName()).path(getOptionsMapNodeName()), true); + if (!dryStates.isEmpty()) { // sanity check + acCap.setAutoDryModeAvailable(true); + dryStates.forEach((cmdKey, cmdValue) -> { + switch (cmdKey) { + case CAP_AC_COMMAND_OFF: + acCap.setAutoDryModeCommandOff(cmdValue); + break; + case CAP_AC_COMMAND_ON: + acCap.setAutoDryModeCommandOn(cmdValue); + } + }); + } + break; + case CAP_AC_AIRCLEAN: + Map airCleanStates = extractOptions( + valuesNode.path(getAirCleanStateNodeName()).path(getOptionsMapNodeName()), true); + if (!airCleanStates.isEmpty()) { + acCap.setAirCleanAvailable(true); + airCleanStates.forEach((cmdKey, cmdValue) -> { + switch (cmdKey) { + case CAP_AC_AIR_CLEAN_COMMAND_OFF: + acCap.setAirCleanModeCommandOff(cmdValue); + break; + case CAP_AC_AIR_CLEAN_COMMAND_ON: + acCap.setAirCleanModeCommandOn(cmdValue); + } + }); + } + break; + case CAP_AC_ENERGYSAVING: + acCap.setEnergySavingAvailable(true); + // there's no definition for this values. Assuming the defaults + acCap.setEnergySavingModeCommandOff("0"); + acCap.setEnergySavingModeCommandOn("1"); + break; + } + }); + } + if (HEAT_PUMP.equals(acCap.getDeviceType())) { + JsonNode supHpAirSwitchNode = valuesNode.path(getHpAirWaterSwitchNodeName()).path(getOptionsMapNodeName()); + if (!supHpAirSwitchNode.isMissingNode()) { + supHpAirSwitchNode.fields().forEachRemaining(r -> { + r.getValue().asText(); + }); + } + } + + JsonNode infoNode = rootNode.get("Info"); + if (infoNode.isMissingNode()) { + logger.warn("No info session defined in the cap data."); + } else { + // try to find monitoring result format + MonitoringResultFormat format = MonitoringResultFormat.getFormatOf(infoNode.path("model").asText()); + if (!MonitoringResultFormat.UNKNOWN_FORMAT.equals(format)) { + acCap.setMonitoringDataFormat(format); + } + } + return acCap; + } + + protected abstract String getOpModeNodeName(); + + protected abstract String getFanSpeedNodeName(); + + protected abstract String getSupOpModeNodeName(); + + protected abstract String getSupFanSpeedNodeName(); + + protected abstract String getJetModeNodeName(); + + protected abstract String getStepUpDownNodeName(); + + protected abstract String getStepLeftRightNodeName(); + + protected abstract String getSupSubRacModeNodeName(); + + protected abstract String getSupRacModeNodeName(); + + protected abstract String getAutoDryStateNodeName(); + + protected abstract String getAirCleanStateNodeName(); + + protected abstract String getOptionsMapNodeName(); + + @Override + public ACCapability getCapabilityInstance() { + return new ACCapability(); + } + + protected abstract String getValuesNodeName(); + + // ===== For HP only ==== + protected abstract String getHpAirWaterSwitchNodeName(); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ExtendedDeviceInfo.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ExtendedDeviceInfo.java new file mode 100644 index 0000000000000..665a280e75d6d --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/ac/ExtendedDeviceInfo.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.ac; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_EXTRA_ATTR_FILTER_MAX_TIME_TO_USE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_EXTRA_ATTR_FILTER_USED_TIME; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_EXTRA_ATTR_INSTANT_POWER; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@link ExtendedDeviceInfo} containing extended information obout the device. In + * AC cases, it holds instant power consumption, filter used in hours and max time to use. + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExtendedDeviceInfo { + private String instantPower = ""; + + @JsonProperty(CAP_EXTRA_ATTR_FILTER_USED_TIME) + @JsonAlias("airState.filterMngStates.useTime") + public String getFilterHoursUsed() { + return filterHoursUsed; + } + + public void setFilterHoursUsed(String filterHoursUsed) { + this.filterHoursUsed = filterHoursUsed; + } + + @JsonProperty(CAP_EXTRA_ATTR_FILTER_MAX_TIME_TO_USE) + @JsonAlias("airState.filterMngStates.maxTime") + public String getFilterHoursMax() { + return filterHoursMax; + } + + public void setFilterHoursMax(String filterHoursMax) { + this.filterHoursMax = filterHoursMax; + } + + private String filterHoursUsed = ""; + private String filterHoursMax = ""; + + /** + * Returns the instant total power consumption + * + * @return the instant total power consumption + */ + @JsonProperty(CAP_EXTRA_ATTR_INSTANT_POWER) + @JsonAlias("airState.energy.totalCurrent") + public String getRawInstantPower() { + return instantPower; + } + + public Double getInstantPower() { + try { + return Double.parseDouble(instantPower); + } catch (NumberFormatException e) { + return 0.0; + } + } + + public void setRawInstantPower(String instantPower) { + this.instantPower = instantPower; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseDefinition.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseDefinition.java new file mode 100644 index 0000000000000..fd7df82897a2a --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseDefinition.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CourseDefinition} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class CourseDefinition { + private String courseName = ""; + // Name of the course this is based on. It's only used for SmartCourses + private String baseCourseName = ""; + private CourseType courseType = CourseType.UNDEF; + private List functions = new ArrayList<>(); + + public String getCourseName() { + return courseName; + } + + public String getBaseCourseName() { + return baseCourseName; + } + + public void setBaseCourseName(String baseCourseName) { + this.baseCourseName = baseCourseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public CourseType getCourseType() { + return courseType; + } + + public void setCourseType(CourseType courseType) { + this.courseType = courseType; + } + + public List getFunctions() { + return functions; + } + + public void setFunctions(List functions) { + this.functions = functions; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseFunction.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseFunction.java new file mode 100644 index 0000000000000..02c9ce40276fb --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseFunction.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CourseFunction} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class CourseFunction { + private String value = ""; + private String defaultValue = ""; + private boolean isSelectable; + private List selectableValues = new ArrayList<>(); + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public boolean isSelectable() { + return isSelectable; + } + + public void setSelectable(boolean selectable) { + isSelectable = selectable; + } + + public List getSelectableValues() { + return selectableValues; + } + + public void setSelectableValues(List selectableValues) { + this.selectableValues = selectableValues; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseType.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseType.java new file mode 100644 index 0000000000000..c548a63b24a32 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/CourseType.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CourseType} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public enum CourseType { + COURSE("Course"), + SMART_COURSE("SmartCourse"), + DOWNLOADED_COURSE("DownloadedCourse"), + UNDEF("Undefined"); + + private final String value; + + CourseType(String s) { + value = s; + } + + public String getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/Utils.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/Utils.java new file mode 100644 index 0000000000000..b5275ec6d6646 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/Utils.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * The {@link Utils} class defines common methods to handle generic washer devices + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class Utils { + + public static Map getGenericCourseDefinitions(JsonNode courseNode, CourseType type, + String notSelectedCourseKey) { + Map coursesDef = new HashMap<>(); + courseNode.fields().forEachRemaining(e -> { + CourseDefinition cd = new CourseDefinition(); + JsonNode thisCourseNode = e.getValue(); + cd.setCourseName(thisCourseNode.path("_comment").textValue()); + if (CourseType.SMART_COURSE.equals(type)) { + cd.setBaseCourseName(thisCourseNode.path("Course").textValue()); + } + cd.setCourseType(type); + if (thisCourseNode.path("function").isArray()) { + // just to be safe here + ArrayNode functions = (ArrayNode) thisCourseNode.path("function"); + List functionList = cd.getFunctions(); + for (JsonNode fNode : functions) { + // map all course functions here + CourseFunction f = new CourseFunction(); + f.setValue(fNode.path("value").textValue()); + f.setDefaultValue(fNode.path("default").textValue()); + JsonNode selectableNode = fNode.path("selectable"); + // only Courses (not SmartCourses or DownloadedCourses) can have selectable functions + f.setSelectable( + !selectableNode.isMissingNode() && selectableNode.isArray() && (type == CourseType.COURSE)); + if (f.isSelectable()) { + List selectableValues = f.getSelectableValues(); + // map values acceptable for this function + for (JsonNode v : selectableNode) { + if (v.isValueNode()) { + selectableValues.add(v.textValue()); + } + } + f.setSelectableValues(selectableValues); + } + functionList.add(f); + } + cd.setFunctions(functionList); + } + coursesDef.put(e.getKey(), cd); + }); + CourseDefinition cdNotSelected = new CourseDefinition(); + cdNotSelected.setCourseType(type); + cdNotSelected.setCourseName("Not Selected"); + coursesDef.put(notSelectedCourseKey, cdNotSelected); + return coursesDef; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/WasherFeatureDefinition.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/WasherFeatureDefinition.java new file mode 100644 index 0000000000000..10efd40899558 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/commons/washers/WasherFeatureDefinition.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDataType; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The WasherFeatureDefinition + * + * @author nemer (nemer.daud@gmail.com) - Initial contribution + */ +@NonNullByDefault +public class WasherFeatureDefinition { + public static FeatureDefinition getBasicFeatureDefinition(String featureName, JsonNode featureNode, + @Nullable String targetChannelId, @Nullable String refChannelId) { + if (featureNode.isMissingNode()) { + return FeatureDefinition.NULL_DEFINITION; + } + FeatureDefinition fd = new FeatureDefinition(); + fd.setName(featureName); + fd.setChannelId(Objects.requireNonNullElse(targetChannelId, "")); + fd.setRefChannelId(Objects.requireNonNullElse(refChannelId, "")); + fd.setLabel(featureName); + return fd; + } + + public static FeatureDefinition setAllValuesMapping(FeatureDefinition fd, JsonNode featureNode) { + fd.setDataType(FeatureDataType.ENUM); + JsonNode valuesMappingNode = featureNode.path("option"); + if (!valuesMappingNode.isMissingNode()) { + + Map valuesMapping = new HashMap<>(); + valuesMappingNode.fields().forEachRemaining(e -> { + // collect values as: + // + // "option":{ + // "0":"@WM_STATE_POWER_OFF_W", + // to "0" -> "@WM_STATE_POWER_OFF_W" + valuesMapping.put(e.getKey(), e.getValue().asText()); + }); + fd.setValuesMapping(valuesMapping); + } + + return fd; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/AbstractDishWasherCapabilityFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/AbstractDishWasherCapabilityFactory.java new file mode 100644 index 0000000000000..01c06a0077f54 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/AbstractDishWasherCapabilityFactory.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapabilityFactory; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringResultFormat; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseDefinition; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseType; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.Utils; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link AbstractDishWasherCapabilityFactory} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractDishWasherCapabilityFactory extends AbstractCapabilityFactory { + + protected abstract String getStateFeatureNodeName(); + + protected abstract String getProcessStateNodeName(); + + protected abstract String getDoorLockFeatureNodeName(); + + protected abstract String getConvertingRulesNodeName(); + + protected abstract String getControlConvertingRulesNodeName(); + + @SuppressWarnings("unused") + protected abstract MonitoringResultFormat getMonitorDataFormat(JsonNode rootNode); + + @Override + public DishWasherCapability create(JsonNode rootNode) throws LGThinqException { + DishWasherCapability dwCap = super.create(rootNode); + JsonNode coursesNode = rootNode.path(getCourseNodeName()); + JsonNode smartCoursesNode = rootNode.path(getSmartCourseNodeName()); + if (coursesNode.isMissingNode()) { + throw new LGThinqException("Course node not present in Capability Json Descriptor"); + } + + Map courses = new HashMap<>(getCourseDefinitions(coursesNode)); + Map smartCourses = new HashMap<>(getSmartCourseDefinitions(smartCoursesNode)); + Map convertedAllCourses = new HashMap<>(); + // change the Key to the reverse MapCourses coming from LG API + BiConsumer, JsonNode> convertCoursesRules = (courseMap, node) -> { + node.fields().forEachRemaining(e -> { + CourseDefinition df = courseMap.get(e.getKey()); + if (df != null) { + convertedAllCourses.put(e.getValue().asText(), df); + } + }); + }; + JsonNode controlConvertingRules = rootNode.path(getConvertingRulesNodeName()).path(getCourseNodeName()) + .path(getControlConvertingRulesNodeName()); + if (!controlConvertingRules.isMissingNode()) { + convertCoursesRules.accept(courses, controlConvertingRules); + } + controlConvertingRules = rootNode.path(getConvertingRulesNodeName()).path(getSmartCourseNodeName()) + .path(getControlConvertingRulesNodeName()); + if (!controlConvertingRules.isMissingNode()) { + convertCoursesRules.accept(smartCourses, controlConvertingRules); + } + + dwCap.setCourses(convertedAllCourses); + + JsonNode monitorValueNode = rootNode.path(getMonitorValueNodeName()); + if (monitorValueNode.isMissingNode()) { + throw new LGThinqException("MonitoringValue node not found in the V2 WashingDryer cap definition."); + } + // mapping possible states + dwCap.setState(newFeatureDefinition(getStateFeatureNodeName(), monitorValueNode)); + dwCap.setProcessState(newFeatureDefinition(getProcessStateNodeName(), monitorValueNode)); + dwCap.setDoorStateFeat(newFeatureDefinition(getDoorLockFeatureNodeName(), monitorValueNode)); + dwCap.setMonitoringDataFormat(getMonitorDataFormat(rootNode)); + return dwCap; + } + + protected Map getCourseDefinitions(JsonNode courseNode) { + return Utils.getGenericCourseDefinitions(courseNode, CourseType.COURSE, getNotSelectedCourseKey()); + } + + protected Map getSmartCourseDefinitions(JsonNode smartCourseNode) { + return Utils.getGenericCourseDefinitions(smartCourseNode, CourseType.SMART_COURSE, getNotSelectedCourseKey()); + } + + protected abstract String getNotSelectedCourseKey(); + + @Override + public final List getSupportedDeviceTypes() { + return List.of(DeviceTypes.DISH_WASHER); + } + + protected abstract String getCourseNodeName(); + + protected abstract String getSmartCourseNodeName(); + + protected abstract String getMonitorValueNodeName(); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherCapability.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherCapability.java new file mode 100644 index 0000000000000..9157615ea42b4 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherCapability.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapability; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseDefinition; + +/** + * The {@link DishWasherCapability} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class DishWasherCapability extends AbstractCapability { + private FeatureDefinition doorState = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition state = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition processState = FeatureDefinition.NULL_DEFINITION; + private Map courses = new LinkedHashMap<>(); + + public FeatureDefinition getProcessState() { + return processState; + } + + public void setProcessState(FeatureDefinition processState) { + this.processState = processState; + } + + public Map getCourses() { + return courses; + } + + public void setCourses(Map courses) { + this.courses = courses; + } + + public FeatureDefinition getStateFeat() { + return state; + } + + public FeatureDefinition getDoorStateFeat() { + return doorState; + } + + public void setDoorStateFeat(FeatureDefinition doorState) { + this.doorState = doorState; + } + + public void setState(FeatureDefinition state) { + this.state = state; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherCapabilityFactoryV2.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherCapabilityFactoryV2.java new file mode 100644 index 0000000000000..59ae3531a41b4 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherCapabilityFactoryV2.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringResultFormat; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.WasherFeatureDefinition; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link DishWasherCapabilityFactoryV2} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class DishWasherCapabilityFactoryV2 extends AbstractDishWasherCapabilityFactory { + + @Override + protected List getSupportedAPIVersions() { + return List.of(LGAPIVerion.V2_0); + } + + protected String getDoorLockFeatureNodeName() { + return "Door"; + } + + @Override + protected String getConvertingRulesNodeName() { + return "ConvertingRule"; + } + + @Override + protected String getControlConvertingRulesNodeName() { + return "ControlConvertingRule"; + } + + @Override + protected FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode, + @Nullable String targetChannelId, @Nullable String refChannelId) { + JsonNode featureNode = featuresNode.path(featureName); + + FeatureDefinition fd; + if ((fd = WasherFeatureDefinition.getBasicFeatureDefinition(featureName, featureNode, targetChannelId, + refChannelId)) == FeatureDefinition.NULL_DEFINITION) { + return fd; + } + // all features from V2 are enums + return WasherFeatureDefinition.setAllValuesMapping(fd, featureNode); + } + + @Override + public DishWasherCapability getCapabilityInstance() { + return new DishWasherCapability(); + } + + @Override + protected String getCourseNodeName() { + return "Course"; + } + + @Override + protected String getSmartCourseNodeName() { + return "SmartCourse"; + } + + @Override + protected String getStateFeatureNodeName() { + return "State"; + } + + @Override + protected String getProcessStateNodeName() { + return "Process"; + } + + @Override + protected MonitoringResultFormat getMonitorDataFormat(JsonNode rootNode) { + // All v2 are Json format + return MonitoringResultFormat.JSON_FORMAT; + } + + @Override + protected Map getCommandsDefinition(JsonNode rootNode) { + return Collections.emptyMap(); + } + + @Override + protected String getNotSelectedCourseKey() { + return "NOT_SELECTED"; + } + + @Override + protected String getMonitorValueNodeName() { + return "Value"; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherSnapshot.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherSnapshot.java new file mode 100644 index 0000000000000..38ad6da589eb4 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherSnapshot.java @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.DW_POWER_OFF_VALUE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.DW_STATE_COMPLETE; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@link DishWasherSnapshot} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@JsonIgnoreProperties(ignoreUnknown = true) +public class DishWasherSnapshot extends AbstractSnapshotDefinition { + private DevicePowerState powerState = DevicePowerState.DV_POWER_UNK; + private String state = ""; + private String processState = ""; + private boolean online; + private String course = ""; + private String smartCourse = ""; + private String doorLock = ""; + private Double remainingHour = 0.00; + private Double remainingMinute = 0.00; + private Double reserveHour = 0.00; + private Double reserveMinute = 0.00; + + @JsonAlias({ "Course" }) + @JsonProperty("course") + public String getCourse() { + return course; + } + + public void setCourse(String course) { + this.course = course; + } + + @JsonProperty("process") + @JsonAlias({ "Process" }) + public String getProcessState() { + return processState; + } + + public void setProcessState(String processState) { + this.processState = processState; + } + + @Override + public DevicePowerState getPowerStatus() { + return powerState; + } + + @Override + public void setPowerStatus(DevicePowerState value) { + this.powerState = value; + } + + @Override + public boolean isOnline() { + return online; + } + + @Override + public void setOnline(boolean online) { + this.online = online; + } + + @JsonProperty("state") + @JsonAlias({ "State" }) + public String getState() { + return state; + } + + @JsonProperty("smartCourse") + @JsonAlias({ "SmartCourse" }) + public String getSmartCourse() { + return smartCourse; + } + + @JsonIgnore + public String getRemainingTime() { + return String.format("%02.0f:%02.0f", getRemainingHour(), getRemainingMinute()); + } + + @JsonIgnore + public String getReserveTime() { + return String.format("%02.0f:%02.0f", getReserveHour(), getReserveMinute()); + } + + @JsonProperty("remainTimeHour") + @JsonAlias({ "Remain_Time_H" }) + public Double getRemainingHour() { + return remainingHour; + } + + public void setRemainingHour(Double remainingHour) { + this.remainingHour = remainingHour; + } + + @JsonProperty("remainTimeMinute") + @JsonAlias({ "Remain_Time_M" }) + public Double getRemainingMinute() { + // Issue in some DW when the remainingMinute stay in 1 after complete in some cases + return DW_STATE_COMPLETE.equals(getState()) ? 0.0 : remainingMinute; + } + + public void setRemainingMinute(Double remainingMinute) { + this.remainingMinute = remainingMinute; + } + + @JsonProperty("reserveTimeHour") + @JsonAlias({ "Reserve_Time_H" }) + public Double getReserveHour() { + return reserveHour; + } + + public void setReserveHour(Double reserveHour) { + this.reserveHour = reserveHour; + } + + @JsonProperty("reserveTimeMinute") + @JsonAlias({ "Reserve_Time_M" }) + public Double getReserveMinute() { + return reserveMinute; + } + + public void setReserveMinute(Double reserveMinute) { + this.reserveMinute = reserveMinute; + } + + public void setSmartCourse(String smartCourse) { + this.smartCourse = smartCourse; + } + + @JsonProperty("door") + @JsonAlias({ "Door" }) + public String getDoorLock() { + return doorLock; + } + + public void setDoorLock(String doorLock) { + this.doorLock = doorLock; + } + + public void setState(String state) { + this.state = state; + if (state.equals(DW_POWER_OFF_VALUE)) { + powerState = DevicePowerState.DV_POWER_OFF; + } else { + powerState = DevicePowerState.DV_POWER_ON; + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherSnapshotBuilder.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherSnapshotBuilder.java new file mode 100644 index 0000000000000..85569123f1e79 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/dishwasher/DishWasherSnapshotBuilder.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.dishwasher; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.DW_SNAPSHOT_WASHER_DRYER_NODE_V2; + +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DefaultSnapshotBuilder; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; + +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * The {@link DishWasherSnapshotBuilder} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class DishWasherSnapshotBuilder extends DefaultSnapshotBuilder { + public DishWasherSnapshotBuilder() { + super(DishWasherSnapshot.class); + } + + @Override + protected DishWasherSnapshot getSnapshot(Map snapMap, CapabilityDefinition capDef) { + DishWasherSnapshot snap; + DeviceTypes type = capDef.getDeviceType(); + LGAPIVerion version = capDef.getDeviceVersion(); + if (!type.equals(DeviceTypes.DISH_WASHER)) { + throw new IllegalArgumentException( + String.format("Device Type %s not supported by this builder. It's most likely a bug.", type)); + } + switch (version) { + case V1_0: + throw new IllegalArgumentException("Version 1.0 for DishWasher is not supported yet."); + case V2_0: + Map dishWasher = Objects.requireNonNull( + objectMapper.convertValue(snapMap.get(DW_SNAPSHOT_WASHER_DRYER_NODE_V2), new TypeReference<>() { + }), "dishwasher node must be present in the snapshot"); + snap = objectMapper.convertValue(dishWasher, snapClass); + snap.setRawData(dishWasher); + return snap; + default: + throw new IllegalArgumentException( + String.format("Version %s for DishWasher is not supported.", version)); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/AbstractFridgeCapabilityFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/AbstractFridgeCapabilityFactory.java new file mode 100644 index 0000000000000..32f09196df84e --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/AbstractFridgeCapabilityFactory.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_CELSIUS_SYMBOL; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_FAHRENHEIT_SYMBOL; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapabilityFactory; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link AbstractFridgeCapabilityFactory} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractFridgeCapabilityFactory extends AbstractCapabilityFactory { + private static final Logger logger = LoggerFactory.getLogger(AbstractFridgeCapabilityFactory.class); + + protected abstract void loadTempNode(JsonNode tempNode, Map capMap, String unit); + + private Map convertCelsius2Fahrenheit(Map celcius) { + Map fMap = new HashMap<>(); + celcius.forEach((k, v) -> { + int c = Integer.parseInt(v); + fMap.put(k, String.valueOf((c * 9 / 5) + 32)); + }); + return fMap; + } + + @Override + public FridgeCapability create(JsonNode rootNode) throws LGThinqException { + FridgeCapability frCap = super.create(rootNode); + JsonNode node = mapper.valueToTree(rootNode); + if (node.isNull()) { + logger.debug("Can't parse json capability for Fridge. The payload has been ignored. Payload:{}", rootNode); + throw new LGThinqException("Can't parse json capability for Fridge. The payload has been ignored"); + } + + JsonNode fridgeTempCNode = node.path(getMonitorValueNodeName()).path(getFridgeTempCNodeName()) + .path(getOptionsNodeName()); + // version 1.4 of refrigerators thinq1 doesn't contain temp. segregated in C and F. + if (fridgeTempCNode.isMissingNode()) { + fridgeTempCNode = node.path(getMonitorValueNodeName()).path(getFridgeTempNodeName()) + .path(getOptionsNodeName()); + } + JsonNode fridgeTempFNode = node.path(getMonitorValueNodeName()).path(getFridgeTempFNodeName()) + .path(getOptionsNodeName()); + JsonNode freezerTempCNode = node.path(getMonitorValueNodeName()).path(getFreezerTempCNodeName()) + .path(getOptionsNodeName()); + if (freezerTempCNode.isMissingNode()) { + freezerTempCNode = node.path(getMonitorValueNodeName()).path(getFreezerTempNodeName()) + .path(getOptionsNodeName()); + } + JsonNode freezerTempFNode = node.path(getMonitorValueNodeName()).path(getFreezerTempFNodeName()) + .path(getOptionsNodeName()); + JsonNode tempUnitNode = node.path(getMonitorValueNodeName()).path(getTempUnitNodeName()) + .path(getOptionsNodeName()); + JsonNode icePlusNode = node.path(getMonitorValueNodeName()).path(getIcePlusNodeName()) + .path(getOptionsNodeName()); + JsonNode freshAirFilterNode = node.path(getMonitorValueNodeName()).path(getFreshAirFilterNodeName()) + .path(getOptionsNodeName()); + JsonNode waterFilterNode = node.path(getMonitorValueNodeName()).path(getWaterFilterNodeName()) + .path(getOptionsNodeName()); + JsonNode expressModeNode = node.path(getMonitorValueNodeName()).path(getExpressModeNodeName()) + .path(getOptionsNodeName()); + JsonNode smartSavingModeNode = node.path(getMonitorValueNodeName()).path(getSmartSavingModeNodeName()) + .path(getOptionsNodeName()); + JsonNode activeSavingNode = node.path(getMonitorValueNodeName()).path(getActiveSavingNodeName()) + .path(getOptionsNodeName()); + JsonNode atLeastOneDoorOpenNode = node.path(getMonitorValueNodeName()).path(getAtLeastOneDoorOpenNodeName()) + .path(getOptionsNodeName()); + if (!node.path(getMonitorValueNodeName()).path(getExpressCoolNodeName()).isMissingNode()) { + frCap.setExpressCoolModePresent(true); + } + if (!node.path(getMonitorValueNodeName()).path(getEcoFriendlyNodeName()).isMissingNode()) { + frCap.setEcoFriendlyModePresent(true); + } + loadTempNode(fridgeTempCNode, frCap.getFridgeTempCMap(), RE_TEMP_UNIT_CELSIUS_SYMBOL); + if (fridgeTempFNode.isMissingNode()) { + frCap.getFridgeTempFMap().putAll(convertCelsius2Fahrenheit(frCap.getFridgeTempCMap())); + } else { + loadTempNode(fridgeTempFNode, frCap.getFridgeTempFMap(), RE_TEMP_UNIT_FAHRENHEIT_SYMBOL); + } + loadTempNode(freezerTempCNode, frCap.getFreezerTempCMap(), RE_TEMP_UNIT_CELSIUS_SYMBOL); + if (freezerTempFNode.isMissingNode()) { + frCap.getFreezerTempFMap().putAll(convertCelsius2Fahrenheit(frCap.getFreezerTempCMap())); + } else { + loadTempNode(freezerTempFNode, frCap.getFreezerTempFMap(), RE_TEMP_UNIT_FAHRENHEIT_SYMBOL); + } + loadTempUnitNode(tempUnitNode, frCap.getTempUnitMap()); + loadIcePlus(icePlusNode, frCap.getIcePlusMap()); + loadFreshAirFilter(freshAirFilterNode, frCap.getFreshAirFilterMap()); + loadWaterFilter(waterFilterNode, frCap.getWaterFilterMap()); + loadExpressFreezeMode(expressModeNode, frCap.getExpressFreezeModeMap()); + loadSmartSavingMode(smartSavingModeNode, frCap.getSmartSavingMap()); + loadActiveSaving(activeSavingNode, frCap.getActiveSavingMap()); + loadAtLeastOneDoorOpen(atLeastOneDoorOpenNode, frCap.getAtLeastOneDoorOpenMap()); + + frCap.getCommandsDefinition().putAll(getCommandsDefinition(node)); + return frCap; + } + + protected abstract void loadTempUnitNode(JsonNode tempUnitNode, Map tempUnitMap); + + protected abstract void loadIcePlus(JsonNode icePlusNode, Map icePlusMap); + + protected abstract void loadFreshAirFilter(JsonNode freshAirFilterNode, Map freshAirFilterMap); + + protected abstract void loadWaterFilter(JsonNode waterFilterNode, Map waterFilterMap); + + protected abstract void loadExpressFreezeMode(JsonNode expressFreezeModeNode, + Map expressFreezeModeMap); + + protected abstract void loadSmartSavingMode(JsonNode smartSavingModeNode, Map smartSavingModeMap); + + protected abstract void loadActiveSaving(JsonNode activeSavingNode, Map activeSavingMap); + + protected abstract void loadAtLeastOneDoorOpen(JsonNode atLeastOneDoorOpenNode, + Map atLeastOneDoorOpenMap); + + @Override + protected List getSupportedDeviceTypes() { + return List.of(DeviceTypes.FRIDGE); + } + + protected abstract String getMonitorValueNodeName(); + + protected abstract String getFridgeTempCNodeName(); + + protected abstract String getFridgeTempNodeName(); + + protected abstract String getFridgeTempFNodeName(); + + protected abstract String getFreezerTempCNodeName(); + + protected abstract String getFreezerTempNodeName(); + + protected abstract String getFreezerTempFNodeName(); + + protected abstract String getTempUnitNodeName(); + + protected abstract String getIcePlusNodeName(); + + protected abstract String getFreshAirFilterNodeName(); + + protected abstract String getWaterFilterNodeName(); + + protected abstract String getExpressModeNodeName(); + + protected abstract String getSmartSavingModeNodeName(); + + protected abstract String getActiveSavingNodeName(); + + protected abstract String getAtLeastOneDoorOpenNodeName(); + + protected abstract String getExpressCoolNodeName(); + + protected abstract String getOptionsNodeName(); + + protected abstract String getEcoFriendlyNodeName(); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/AbstractFridgeSnapshot.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/AbstractFridgeSnapshot.java new file mode 100644 index 0000000000000..7ae7c18ce582a --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/AbstractFridgeSnapshot.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; + +/** + * The {@link AbstractFridgeSnapshot} + * + * @author Nemer Daud - Initial contribution + * @author Arne Seime - Complementary sensors + */ +@NonNullByDefault +public abstract class AbstractFridgeSnapshot extends AbstractSnapshotDefinition { + public abstract String getTempUnit(); + + public abstract String getFridgeStrTemp(); + + public abstract String getFreezerStrTemp(); + + public abstract String getDoorStatus(); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCanonicalCapability.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCanonicalCapability.java new file mode 100644 index 0000000000000..1c8263930b8be --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCanonicalCapability.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapability; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; + +/** + * The {@link FridgeCanonicalCapability} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class FridgeCanonicalCapability extends AbstractCapability + implements FridgeCapability { + + private final Map fridgeTempCMap = new LinkedHashMap(); + private final Map fridgeTempFMap = new LinkedHashMap(); + private final Map freezerTempCMap = new LinkedHashMap(); + private final Map freezerTempFMap = new LinkedHashMap(); + private final Map tempUnitMap = new LinkedHashMap(); + private final Map icePlusMap = new LinkedHashMap(); + private final Map freshAirFilterMap = new LinkedHashMap(); + private final Map waterFilterMap = new LinkedHashMap(); + private final Map expressFreezeModeMap = new LinkedHashMap(); + private final Map smartSavingMap = new LinkedHashMap(); + private final Map activeSavingMap = new LinkedHashMap(); + private final Map atLeastOneDoorOpenMap = new LinkedHashMap<>(); + private final Map commandsDefinition = new LinkedHashMap<>(); + private boolean isExpressCoolModePresent = false; + private boolean isEcoFriendlyModePresent = false; + + public void setExpressCoolModePresent(boolean expressCoolModePresent) { + isExpressCoolModePresent = expressCoolModePresent; + } + + @Override + public boolean isEcoFriendlyModePresent() { + return isEcoFriendlyModePresent; + } + + @Override + public void setEcoFriendlyModePresent(boolean isEcoFriendlyModePresent) { + this.isEcoFriendlyModePresent = isEcoFriendlyModePresent; + } + + public Map getFridgeTempCMap() { + return fridgeTempCMap; + } + + public Map getFridgeTempFMap() { + return fridgeTempFMap; + } + + public Map getFreezerTempCMap() { + return freezerTempCMap; + } + + public Map getFreezerTempFMap() { + return freezerTempFMap; + } + + @Override + public Map getTempUnitMap() { + return tempUnitMap; + } + + @Override + public Map getIcePlusMap() { + return icePlusMap; + } + + @Override + public Map getFreshAirFilterMap() { + return freshAirFilterMap; + } + + @Override + public Map getWaterFilterMap() { + return waterFilterMap; + } + + @Override + public Map getExpressFreezeModeMap() { + return expressFreezeModeMap; + } + + @Override + public Map getSmartSavingMap() { + return smartSavingMap; + } + + @Override + public Map getActiveSavingMap() { + return activeSavingMap; + } + + @Override + public Map getAtLeastOneDoorOpenMap() { + return atLeastOneDoorOpenMap; + } + + public Map getCommandsDefinition() { + return commandsDefinition; + } + + @Override + public boolean isExpressCoolModePresent() { + return isExpressCoolModePresent; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCanonicalSnapshot.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCanonicalSnapshot.java new file mode 100644 index 0000000000000..8d20b3f6221b7 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCanonicalSnapshot.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.FREEZER_TEMPERATURE_IGNORE_VALUE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.FRIDGE_TEMPERATURE_IGNORE_VALUE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_CELSIUS; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_CELSIUS_SYMBOL; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_FAHRENHEIT; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_TEMP_UNIT_FAHRENHEIT_SYMBOL; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@link FridgeCanonicalSnapshot} + * This map the snapshot result from Washing Machine devices + * This json payload come with path: snapshot->fridge, but this POJO expects + * to map field below washerDryer + * + * @author Nemer Daud - Initial contribution + * @author Arne Seime - Complementary sensors + */ +@NonNullByDefault +@JsonIgnoreProperties(ignoreUnknown = true) +public class FridgeCanonicalSnapshot extends AbstractFridgeSnapshot { + + private boolean online; + private Double fridgeTemp = FRIDGE_TEMPERATURE_IGNORE_VALUE; + private Double freezerTemp = FREEZER_TEMPERATURE_IGNORE_VALUE; + private String tempUnit = RE_TEMP_UNIT_CELSIUS; // celsius as default + + private String doorStatus = ""; + private String waterFilterUsedMonth = ""; + private String freshAirFilterState = ""; + + private String expressMode = ""; + private String expressCoolMode = ""; + private String ecoFriendlyMode = ""; + + @JsonProperty("ecoFriendly") + public String getEcoFriendlyMode() { + return ecoFriendlyMode; + } + + public void setEcoFriendlyMode(String ecoFriendlyMode) { + this.ecoFriendlyMode = ecoFriendlyMode; + } + + @JsonProperty("expressMode") + public String getExpressMode() { + return expressMode; + } + + public void setExpressMode(String expressMode) { + this.expressMode = expressMode; + } + + @JsonProperty("atLeastOneDoorOpen") + @JsonAlias("DoorOpenState") + public String getDoorStatus() { + return doorStatus; + } + + public void setDoorStatus(String doorStatus) { + this.doorStatus = doorStatus; + } + + @Override + @JsonAlias({ "TempUnit" }) + @JsonProperty("tempUnit") + public String getTempUnit() { + return tempUnit; + } + + private String getStrTempWithUnit(Double temp) { + return temp.intValue() + (RE_TEMP_UNIT_CELSIUS.equals(tempUnit) ? " " + RE_TEMP_UNIT_CELSIUS_SYMBOL + : (RE_TEMP_UNIT_FAHRENHEIT).equals(tempUnit) ? " " + RE_TEMP_UNIT_FAHRENHEIT_SYMBOL : ""); + } + + @Override + @JsonIgnore + public String getFridgeStrTemp() { + return getStrTempWithUnit(getFridgeTemp()); + } + + @Override + @JsonIgnore + public String getFreezerStrTemp() { + return getStrTempWithUnit(getFreezerTemp()); + } + + public void setTempUnit(String tempUnit) { + this.tempUnit = tempUnit; + } + + @JsonAlias({ "TempRefrigerator" }) + @JsonProperty("fridgeTemp") + public Double getFridgeTemp() { + return fridgeTemp; + } + + public void setFridgeTemp(Double fridgeTemp) { + this.fridgeTemp = fridgeTemp; + } + + @JsonAlias({ "TempFreezer" }) + @JsonProperty("freezerTemp") + public Double getFreezerTemp() { + return freezerTemp; + } + + public void setFreezerTemp(Double freezerTemp) { + this.freezerTemp = freezerTemp; + } + + @Override + public DevicePowerState getPowerStatus() { + return isOnline() ? DevicePowerState.DV_POWER_ON : DevicePowerState.DV_POWER_OFF; + } + + @Override + public void setPowerStatus(DevicePowerState value) { + } + + @JsonAlias({ "WaterFilterUsedMonth" }) + @JsonProperty("waterFilter") + public String getWaterFilterUsedMonth() { + return waterFilterUsedMonth; + } + + @JsonAlias({ "FreshAirFilter" }) + @JsonProperty("freshAirFilter") + public String getFreshAirFilterState() { + return freshAirFilterState; + } + + public void setWaterFilterUsedMonth(String waterFilterUsedMonth) { + this.waterFilterUsedMonth = waterFilterUsedMonth; + } + + public void setFreshAirFilterState(String freshAirFilterState) { + this.freshAirFilterState = freshAirFilterState; + } + + @Override + public boolean isOnline() { + return online; + } + + @Override + public void setOnline(boolean online) { + this.online = online; + } + + @JsonProperty("expressFridge") + public String getExpressCoolMode() { + return expressCoolMode; + } + + public void setExpressCoolMode(String expressCoolMode) { + this.expressCoolMode = expressCoolMode; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapability.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapability.java new file mode 100644 index 0000000000000..4ebb3fc0dbe5a --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapability.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; + +/** + * The {@link FridgeCapability} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public interface FridgeCapability extends CapabilityDefinition { + + Map getFridgeTempCMap(); + + Map getFridgeTempFMap(); + + Map getFreezerTempCMap(); + + Map getFreezerTempFMap(); + + Map getTempUnitMap(); + + Map getIcePlusMap(); + + Map getFreshAirFilterMap(); + + Map getWaterFilterMap(); + + Map getExpressFreezeModeMap(); + + Map getSmartSavingMap(); + + Map getActiveSavingMap(); + + Map getAtLeastOneDoorOpenMap(); + + Map getCommandsDefinition(); + + boolean isExpressCoolModePresent(); + + void setExpressCoolModePresent(boolean isPresent); + + boolean isEcoFriendlyModePresent(); + + void setEcoFriendlyModePresent(boolean isPresent); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapabilityFactoryV1.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapabilityFactoryV1.java new file mode 100644 index 0000000000000..7290e6efb3643 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapabilityFactoryV1.java @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_FRESH_AIR_FILTER_MAP; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_ON_OFF; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_SMART_SAVING_MODE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_WATER_FILTER_USED_POSTFIX; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link FridgeCapabilityFactoryV1} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class FridgeCapabilityFactoryV1 extends AbstractFridgeCapabilityFactory { + + @Override + protected FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode, + @Nullable String targetChannelId, @Nullable String refChannelId) { + return FeatureDefinition.NULL_DEFINITION; + } + + @Override + protected Map getCommandsDefinition(JsonNode rootNode) { + return getCommandsDefinitionV1(rootNode); + } + + private void loadGenericFeatNode(JsonNode featNode, Map capMap, + final Map constantsMap) { + featNode.fields().forEachRemaining(f -> { + // for each node like ' "1": {"index" : 1, "label" : "7", "_comment" : ""} ' + String translatedValue = constantsMap.get(f.getValue().asText()); + translatedValue = translatedValue == null ? f.getValue().asText() : translatedValue; + capMap.put(f.getKey(), translatedValue); + }); + } + + protected void loadTempNode(JsonNode tempNode, Map capMap, String unit) { + loadGenericFeatNode(tempNode, capMap, Collections.emptyMap()); + } + + @Override + protected void loadTempUnitNode(JsonNode tempUnitNode, Map tempUnitMap) { + loadGenericFeatNode(tempUnitNode, tempUnitMap, Collections.emptyMap()); + } + + @Override + protected void loadIcePlus(JsonNode icePlusNode, Map icePlusMap) { + loadGenericFeatNode(icePlusNode, icePlusMap, CAP_RE_ON_OFF); + } + + @Override + protected void loadFreshAirFilter(JsonNode freshAirFilterNode, Map freshAirFilterMap) { + loadGenericFeatNode(freshAirFilterNode, freshAirFilterMap, CAP_RE_FRESH_AIR_FILTER_MAP); + } + + @Override + protected void loadWaterFilter(JsonNode waterFilterNode, Map waterFilterMap) { + int minValue = waterFilterNode.path("min").asInt(0); + int maxValue = waterFilterNode.path("max").asInt(6); + for (int i = minValue; i <= maxValue; i++) { + waterFilterMap.put(String.valueOf(i), i + CAP_RE_WATER_FILTER_USED_POSTFIX); + } + } + + @Override + protected void loadExpressFreezeMode(JsonNode expressFreezeModeNode, Map expressFreezeModeMap) { + // not supported + } + + @Override + protected void loadSmartSavingMode(JsonNode smartSavingModeNode, Map smartSavingModeMap) { + loadGenericFeatNode(smartSavingModeNode, smartSavingModeMap, CAP_RE_SMART_SAVING_MODE); + } + + @Override + protected void loadActiveSaving(JsonNode activeSavingNode, Map activeSavingMap) { + int minValue = activeSavingNode.path("min").asInt(0); + int maxValue = activeSavingNode.path("max").asInt(3); + for (int i = minValue; i <= maxValue; i++) { + activeSavingMap.put(String.valueOf(i), String.valueOf(i)); + } + } + + @Override + protected void loadAtLeastOneDoorOpen(JsonNode atLeastOneDoorOpenNode, Map atLeastOneDoorOpenMap) { + loadGenericFeatNode(atLeastOneDoorOpenNode, atLeastOneDoorOpenMap, Collections.emptyMap()); + } + + @Override + protected List getSupportedAPIVersions() { + return List.of(LGAPIVerion.V1_0); + } + + @Override + public FridgeCapability getCapabilityInstance() { + return new FridgeCanonicalCapability(); + } + + @Override + protected String getMonitorValueNodeName() { + return "Value"; + } + + @Override + protected String getFridgeTempCNodeName() { + return "TempRefrigerator_C"; + } + + protected String getFridgeTempNodeName() { + return "TempRefrigerator"; + } + + @Override + protected String getFridgeTempFNodeName() { + return "TempRefrigerator_F"; + } + + @Override + protected String getFreezerTempCNodeName() { + return "TempFreezer_C"; + } + + protected String getFreezerTempNodeName() { + return "TempFreezer"; + } + + @Override + protected String getFreezerTempFNodeName() { + return "TempFreezer_F"; + } + + @Override + protected String getOptionsNodeName() { + return "option"; + } + + @Override + protected String getEcoFriendlyNodeName() { + return "UNSUPPORTED"; + } + + protected String getTempUnitNodeName() { + return "TempUnit"; + } + + protected String getIcePlusNodeName() { + return "IcePlus"; + } + + @Override + protected String getFreshAirFilterNodeName() { + return "FreshAirFilter"; + } + + @Override + protected String getWaterFilterNodeName() { + return "WaterFilterUsedMonth"; + } + + @Override + protected String getExpressModeNodeName() { + return "UNSUPPORTED"; + } + + @Override + protected String getSmartSavingModeNodeName() { + return "SmartSavingMode"; + } + + @Override + protected String getActiveSavingNodeName() { + return "SmartSavingModeStatus"; + } + + @Override + protected String getAtLeastOneDoorOpenNodeName() { + return "DoorOpenState"; + } + + @Override + protected String getExpressCoolNodeName() { + return "UNSUPPORTED"; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapabilityFactoryV2.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapabilityFactoryV2.java new file mode 100644 index 0000000000000..d881b1474720f --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeCapabilityFactoryV2.java @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_EXPRESS_FREEZE_MODES; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_FRESH_AIR_FILTER_MAP; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_LABEL_CLOSE_OPEN; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_LABEL_ON_OFF; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_SMART_SAVING_V2_MODE; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_TEMP_UNIT_V2_MAP; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.CAP_RE_WATER_FILTER; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link FridgeCapabilityFactoryV2} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class FridgeCapabilityFactoryV2 extends AbstractFridgeCapabilityFactory { + @Override + protected FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode, + @Nullable String targetChannelId, @Nullable String refChannelId) { + return FeatureDefinition.NULL_DEFINITION; + } + + @Override + protected Map getCommandsDefinition(JsonNode rootNode) { + // doesn't meter command definition for V2 + return Collections.emptyMap(); + } + + @Override + protected List getSupportedAPIVersions() { + return List.of(LGAPIVerion.V2_0); + } + + @Override + public FridgeCapability getCapabilityInstance() { + return new FridgeCanonicalCapability(); + } + + private void loadGenericFeatNode(JsonNode featNode, Map capMap, + final Map constantsMap) { + featNode.fields().forEachRemaining(f -> { + // for each node like ' "1": {"index" : 1, "label" : "7", "_comment" : ""} ' + if (!"IGNORE".equals(f.getKey())) { + String translatedValue = constantsMap.get(f.getValue().path("label").asText()); + translatedValue = translatedValue == null ? f.getValue().path("label").asText() : translatedValue; + capMap.put(f.getKey(), translatedValue); + } + }); + } + + @Override + protected void loadTempNode(JsonNode tempNode, Map capMap, String unit) { + loadGenericFeatNode(tempNode, capMap, Collections.emptyMap()); + } + + @Override + protected void loadTempUnitNode(JsonNode tempUnitNode, Map tempUnitMap) { + tempUnitMap.putAll(CAP_RE_TEMP_UNIT_V2_MAP); + } + + @Override + protected void loadIcePlus(JsonNode icePlusNode, Map icePlusMap) { + // not supported + } + + @Override + protected void loadFreshAirFilter(JsonNode freshAirFilterNode, Map freshAirFilterMap) { + loadGenericFeatNode(freshAirFilterNode, freshAirFilterMap, CAP_RE_FRESH_AIR_FILTER_MAP); + } + + @Override + protected void loadWaterFilter(JsonNode waterFilterNode, Map waterFilterMap) { + loadGenericFeatNode(waterFilterNode, waterFilterMap, CAP_RE_WATER_FILTER); + } + + @Override + protected void loadExpressFreezeMode(JsonNode expressFreezeModeNode, Map expressFreezeModeMap) { + loadGenericFeatNode(expressFreezeModeNode, expressFreezeModeMap, CAP_RE_EXPRESS_FREEZE_MODES); + } + + @Override + protected void loadSmartSavingMode(JsonNode smartSavingModeNode, Map smartSavingModeMap) { + loadGenericFeatNode(smartSavingModeNode, smartSavingModeMap, CAP_RE_SMART_SAVING_V2_MODE); + } + + @Override + protected void loadActiveSaving(JsonNode activeSavingNode, Map activeSavingMap) { + loadGenericFeatNode(activeSavingNode, activeSavingMap, CAP_RE_LABEL_ON_OFF); + } + + @Override + protected void loadAtLeastOneDoorOpen(JsonNode atLeastOneDoorOpenNode, Map atLeastOneDoorOpenMap) { + loadGenericFeatNode(atLeastOneDoorOpenNode, atLeastOneDoorOpenMap, CAP_RE_LABEL_CLOSE_OPEN); + } + + @Override + protected String getMonitorValueNodeName() { + return "MonitoringValue"; + } + + @Override + protected String getFridgeTempCNodeName() { + return "fridgeTemp_C"; + } + + protected String getFridgeTempNodeName() { + throw new UnsupportedOperationException("Fridge Thinq2 doesn't support FridgeTemp node. It most likely a bug"); + } + + @Override + protected String getFridgeTempFNodeName() { + return "fridgeTemp_F"; + } + + @Override + protected String getFreezerTempCNodeName() { + return "freezerTemp_C"; + } + + @Override + protected String getFreezerTempNodeName() { + throw new UnsupportedOperationException("Fridge Thinq2 doesn't support FreezerTemp node. It most likely a bug"); + } + + @Override + protected String getFreezerTempFNodeName() { + return "freezerTemp_F"; + } + + protected String getTempUnitNodeName() { + return "tempUnit"; + } + + protected String getIcePlusNodeName() { + return "UNSUPPORTED"; + } + + @Override + protected String getFreshAirFilterNodeName() { + return "freshAirFilter"; + } + + @Override + protected String getWaterFilterNodeName() { + return "waterFilter"; + } + + @Override + protected String getExpressModeNodeName() { + return "expressMode"; + } + + @Override + protected String getSmartSavingModeNodeName() { + return "smartSavingMode"; + } + + @Override + protected String getActiveSavingNodeName() { + return "activeSaving"; + } + + @Override + protected String getAtLeastOneDoorOpenNodeName() { + return "atLeastOneDoorOpen"; + } + + @Override + protected String getExpressCoolNodeName() { + return "expressFridge"; + } + + @Override + protected String getOptionsNodeName() { + return "valueMapping"; + } + + @Override + protected String getEcoFriendlyNodeName() { + return "ecoFriendly"; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeSnapshotBuilder.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeSnapshotBuilder.java new file mode 100644 index 0000000000000..51b8806a7ebc9 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/fridge/FridgeSnapshotBuilder.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.fridge; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.RE_SNAPSHOT_NODE_V2; +import static org.openhab.binding.lgthinq.lgservices.model.DeviceTypes.FRIDGE; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DefaultSnapshotBuilder; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringBinaryProtocol; + +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * The {@link FridgeSnapshotBuilder} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class FridgeSnapshotBuilder extends DefaultSnapshotBuilder { + public FridgeSnapshotBuilder() { + super(FridgeCanonicalSnapshot.class); + } + + @Override + public FridgeCanonicalSnapshot createFromBinary(String binaryData, List prot, + CapabilityDefinition capDef) throws LGThinqUnmarshallException, LGThinqApiException { + return super.createFromBinary(binaryData, prot, capDef); + } + + @Override + protected FridgeCanonicalSnapshot getSnapshot(Map snapMap, CapabilityDefinition capDef) { + FridgeCanonicalSnapshot snap; + if (FRIDGE.equals(capDef.getDeviceType())) { + switch (capDef.getDeviceVersion()) { + case V1_0: + throw new IllegalArgumentException("Version 1.0 for Fridge driver is not supported yet."); + case V2_0: { + Map refMap = Objects.requireNonNull( + objectMapper.convertValue(snapMap.get(RE_SNAPSHOT_NODE_V2), new TypeReference<>() { + }), "washerDryer node must be present in the snapshot"); + snap = objectMapper.convertValue(refMap, snapClass); + snap.setRawData(snapMap); + return snap; + } + default: + throw new IllegalArgumentException("Version informed is not supported for the Fridge driver."); + } + } + + throw new IllegalStateException("Snapshot for device type " + capDef.getDeviceType() + + " not supported for this builder. It most likely a bug"); + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/AbstractWasherDryerCapabilityFactory.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/AbstractWasherDryerCapabilityFactory.java new file mode 100644 index 0000000000000..dc2dabd197c6e --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/AbstractWasherDryerCapabilityFactory.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer; + +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_RINSE; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_SPIN; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_REMOTE_START_TEMP; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_RINSE_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_SPIN_ID; +import static org.openhab.binding.lgthinq.internal.LGThinQBindingConstants.CHANNEL_WMD_TEMP_LEVEL_ID; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WM_LOST_WASHING_STATE_KEY; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WM_LOST_WASHING_STATE_VALUE; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapabilityFactory; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringResultFormat; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseDefinition; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseType; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.Utils; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link AbstractWasherDryerCapabilityFactory} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractWasherDryerCapabilityFactory extends AbstractCapabilityFactory { + + protected abstract String getStateFeatureNodeName(); + + protected abstract String getProcessStateNodeName(); + + protected abstract String getPreStateFeatureNodeName(); + + // --- Selectable features ----- + protected abstract String getRinseFeatureNodeName(); + + protected abstract String getTemperatureFeatureNodeName(); + + protected abstract String getSpinFeatureNodeName(); + + // ------------------------------ + protected abstract String getSoilWashFeatureNodeName(); + + protected abstract String getDoorLockFeatureNodeName(); + + protected abstract MonitoringResultFormat getMonitorDataFormat(JsonNode rootNode); + + protected abstract String getCommandRemoteStartNodeName(); + + protected abstract String getCommandStopNodeName(); + + protected abstract String getCommandWakeUpNodeName(); + + protected abstract String getDefaultCourseIdNodeName(); + + @Override + public WasherDryerCapability create(JsonNode rootNode) throws LGThinqException { + WasherDryerCapability wdCap = super.create(rootNode); + JsonNode coursesNode = rootNode.path(getCourseNodeName(rootNode)); + JsonNode smartCoursesNode = rootNode.path(getSmartCourseNodeName(rootNode)); + if (coursesNode.isMissingNode()) { + throw new LGThinqException("Course node not present in Capability Json Descriptor"); + } + + Map allCourses = new HashMap<>(getCourseDefinitions(coursesNode)); + allCourses.putAll(getSmartCourseDefinitions(smartCoursesNode)); + wdCap.setCourses(allCourses); + + JsonNode monitorValueNode = rootNode.path(getMonitorValueNodeName()); + if (monitorValueNode.isMissingNode()) { + throw new LGThinqException("MonitoringValue node not found in the V2 WashingDryer cap definition."); + } + // mapping possible states + FeatureDefinition fd = newFeatureDefinition(getStateFeatureNodeName(), monitorValueNode); + fd.getValuesMapping().put(WM_LOST_WASHING_STATE_KEY, WM_LOST_WASHING_STATE_VALUE); + wdCap.setState(fd); + wdCap.setProcessState(newFeatureDefinition(getProcessStateNodeName(), monitorValueNode)); + // --- Selectable features ----- + wdCap.setRinseFeat(newFeatureDefinition(getRinseFeatureNodeName(), monitorValueNode, + CHANNEL_WMD_REMOTE_START_RINSE, CHANNEL_WMD_RINSE_ID)); + wdCap.setTemperatureFeat(newFeatureDefinition(getTemperatureFeatureNodeName(), monitorValueNode, + CHANNEL_WMD_REMOTE_START_TEMP, CHANNEL_WMD_TEMP_LEVEL_ID)); + wdCap.setSpinFeat(newFeatureDefinition(getSpinFeatureNodeName(), monitorValueNode, + CHANNEL_WMD_REMOTE_START_SPIN, CHANNEL_WMD_SPIN_ID)); + // ---------------------------- + wdCap.setDryLevel(newFeatureDefinition(getDryLevelNodeName(), monitorValueNode)); + wdCap.setSoilWash(newFeatureDefinition(getSoilWashFeatureNodeName(), monitorValueNode)); + wdCap.setCommandsDefinition(getCommandsDefinition(rootNode)); + // DoorLock feat can be in alone (v2) or inside Options node (v1) + if (monitorValueNode.get(getDoorLockFeatureNodeName()) != null + || hasFeatInOptions(getDoorLockFeatureNodeName(), monitorValueNode)) { + wdCap.setHasDoorLook(true); + } + wdCap.setDefaultCourseFieldName(getConfigCourseType(rootNode)); + wdCap.setDefaultSmartCourseFeatName(getConfigSmartCourseType(rootNode)); + wdCap.setCommandStop(getCommandStopNodeName()); + wdCap.setCommandRemoteStart(getCommandRemoteStartNodeName()); + wdCap.setCommandWakeUp(getCommandWakeUpNodeName()); + // custom feature values map. + wdCap.setFeatureDefinitionMap( + Map.of(getTemperatureFeatureNodeName(), new WasherDryerCapability.TemperatureFeatureFunction(), + getRinseFeatureNodeName(), new WasherDryerCapability.RinseFeatureFunction(), + getSpinFeatureNodeName(), new WasherDryerCapability.SpinFeatureFunction())); + wdCap.setMonitoringDataFormat(getMonitorDataFormat(rootNode)); + wdCap.setDefaultCourseId(rootNode.path("Config").path(getDefaultCourseIdNodeName()).asText()); + return wdCap; + } + + protected abstract boolean hasFeatInOptions(String featName, JsonNode monitoringValueNode); + + protected Map getCourseDefinitions(JsonNode courseNode) { + return Utils.getGenericCourseDefinitions(courseNode, CourseType.COURSE, getNotSelectedCourseKey()); + } + + protected Map getSmartCourseDefinitions(JsonNode smartCourseNode) { + return Utils.getGenericCourseDefinitions(smartCourseNode, CourseType.SMART_COURSE, getNotSelectedCourseKey()); + } + + protected abstract String getDryLevelNodeName(); + + protected abstract String getNotSelectedCourseKey(); + + @Override + public final List getSupportedDeviceTypes() { + return List.of(DeviceTypes.WASHERDRYER_MACHINE, DeviceTypes.DRYER); + } + + protected abstract String getCourseNodeName(JsonNode rootNode); + + protected abstract String getSmartCourseNodeName(JsonNode rootNode); + + protected abstract String getDefaultCourse(JsonNode rootNode); + + protected abstract String getRemoteFeatName(); + + protected abstract String getStandByFeatName(); + + protected abstract String getConfigCourseType(JsonNode rootNode); + + protected abstract String getConfigSmartCourseType(JsonNode rootNote); + + protected abstract String getConfigDownloadCourseType(JsonNode rootNode); + + protected abstract String getMonitorValueNodeName(); +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapability.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapability.java new file mode 100644 index 0000000000000..b2203587a2f23 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapability.java @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.AbstractCapability; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.CourseDefinition; + +/** + * The {@link WasherDryerCapability} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class WasherDryerCapability extends AbstractCapability { + private String defaultCourseFieldName = ""; + private String doorLockFeatName = ""; + private String childLockFeatName = ""; + private String defaultSmartCourseFieldName = ""; + private String commandRemoteStart = ""; + private String remoteStartFeatName = ""; + private String commandWakeUp = ""; + private String commandStop = ""; + private String defaultCourseId = ""; + private FeatureDefinition state = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition soilWash = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition spin = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition temperature = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition rinse = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition error = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition dryLevel = FeatureDefinition.NULL_DEFINITION; + private FeatureDefinition processState = FeatureDefinition.NULL_DEFINITION; + private boolean hasDoorLook; + private Map commandsDefinition = new HashMap<>(); + private Map courses = new LinkedHashMap<>(); + + static class RinseFeatureFunction implements Function { + @Override + public FeatureDefinition apply(WasherDryerCapability c) { + return c.getRinseFeat(); + } + } + + static class TemperatureFeatureFunction implements Function { + @Override + public FeatureDefinition apply(WasherDryerCapability c) { + return c.getTemperatureFeat(); + } + } + + static class SpinFeatureFunction implements Function { + @Override + public FeatureDefinition apply(WasherDryerCapability c) { + return c.getSpinFeat(); + } + } + + public String getDefaultCourseId() { + return defaultCourseId; + } + + public void setDefaultCourseId(String defaultCourseId) { + this.defaultCourseId = defaultCourseId; + } + + public Map getCommandsDefinition() { + return commandsDefinition; + } + + public FeatureDefinition getDryLevel() { + return dryLevel; + } + + public String getCommandStop() { + return commandStop; + } + + public void setCommandStop(String commandStop) { + this.commandStop = commandStop; + } + + public String getCommandRemoteStart() { + return commandRemoteStart; + } + + public void setCommandRemoteStart(String commandRemoteStart) { + this.commandRemoteStart = commandRemoteStart; + } + + public String getCommandWakeUp() { + return commandWakeUp; + } + + public void setCommandWakeUp(String commandWakeUp) { + this.commandWakeUp = commandWakeUp; + } + + public void setDryLevel(FeatureDefinition dryLevel) { + this.dryLevel = dryLevel; + } + + public FeatureDefinition getProcessState() { + return processState; + } + + public void setProcessState(FeatureDefinition processState) { + this.processState = processState; + } + + public void setCommandsDefinition(Map commandsDefinition) { + this.commandsDefinition = commandsDefinition; + } + + public Map getCourses() { + return courses; + } + + public void setCourses(Map courses) { + this.courses = courses; + } + + public FeatureDefinition getStateFeat() { + return state; + } + + public boolean hasDoorLook() { + return this.hasDoorLook; + } + + public void setHasDoorLook(boolean hasDoorLook) { + this.hasDoorLook = hasDoorLook; + } + + public void setState(FeatureDefinition state) { + this.state = state; + } + + public FeatureDefinition getSoilWash() { + return soilWash; + } + + public void setSoilWash(FeatureDefinition soilWash) { + this.soilWash = soilWash; + } + + public FeatureDefinition getSpinFeat() { + return spin; + } + + public void setSpinFeat(FeatureDefinition spin) { + this.spin = spin; + } + + public FeatureDefinition getTemperatureFeat() { + return temperature; + } + + public void setTemperatureFeat(FeatureDefinition temperature) { + this.temperature = temperature; + } + + public FeatureDefinition getRinseFeat() { + return rinse; + } + + public void setRinseFeat(FeatureDefinition rinse) { + this.rinse = rinse; + } + + public FeatureDefinition getError() { + return error; + } + + public void setError(FeatureDefinition error) { + this.error = error; + } + + public String getDefaultCourseFieldName() { + return defaultCourseFieldName; + } + + public void setDefaultCourseFieldName(String defaultCourseFieldName) { + this.defaultCourseFieldName = defaultCourseFieldName; + } + + public String getDefaultSmartCourseFeatName() { + return defaultSmartCourseFieldName; + } + + public void setDefaultSmartCourseFeatName(String defaultSmartCourseFieldName) { + this.defaultSmartCourseFieldName = defaultSmartCourseFieldName; + } + + public String getRemoteStartFeatName() { + return remoteStartFeatName; + } + + public void setRemoteStartFeatName(String remoteStartFeatName) { + this.remoteStartFeatName = remoteStartFeatName; + } + + public String getChildLockFeatName() { + return childLockFeatName; + } + + public void setChildLockFeatName(String childLockFeatName) { + this.childLockFeatName = childLockFeatName; + } + + public String getDoorLockFeatName() { + return doorLockFeatName; + } + + public void setDoorLockFeatName(String doorLockFeatName) { + this.doorLockFeatName = doorLockFeatName; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapabilityFactoryV1.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapabilityFactoryV1.java new file mode 100644 index 0000000000000..390f5e0f543ba --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapabilityFactoryV1.java @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringResultFormat; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.WasherFeatureDefinition; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * The {@link WasherDryerCapabilityFactoryV1} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class WasherDryerCapabilityFactoryV1 extends AbstractWasherDryerCapabilityFactory { + + @Override + public WasherDryerCapability create(JsonNode rootNode) throws LGThinqException { + WasherDryerCapability cap = super.create(rootNode); + cap.setRemoteStartFeatName("RemoteStart"); + cap.setChildLockFeatName("ChildLock"); + cap.setDoorLockFeatName("DoorLock"); + return cap; + } + + @Override + protected boolean hasFeatInOptions(String featName, JsonNode monitoringValueNode) { + for (String optionNode : new String[] { "Option1", "Option2" }) { + JsonNode arrNode = monitoringValueNode.path(optionNode).path("option"); + if (arrNode.isArray()) { + for (JsonNode v : arrNode) { + if (v.asText().equals(featName)) { + return true; + } + } + } + } + return false; + } + + @Override + protected String getStateFeatureNodeName() { + return "State"; + } + + @Override + protected String getProcessStateNodeName() { + return "PreState"; + } + + @Override + protected String getPreStateFeatureNodeName() { + return "PreState"; + } + + @Override + protected String getRinseFeatureNodeName() { + return "RinseOption"; + } + + @Override + protected String getTemperatureFeatureNodeName() { + return "WaterTemp"; + } + + @Override + protected String getSpinFeatureNodeName() { + return "SpinSpeed"; + } + + @Override + protected String getSoilWashFeatureNodeName() { + return "Wash"; + } + + @Override + protected String getDoorLockFeatureNodeName() { + return "DoorLock"; + } + + @Override + protected MonitoringResultFormat getMonitorDataFormat(JsonNode rootNode) { + String type = rootNode.path("Monitoring").path("type").textValue(); + return MonitoringResultFormat.getFormatOf(Objects.requireNonNullElse(type, "")); + } + + @Override + protected Map getCommandsDefinition(JsonNode rootNode) { + return getCommandsDefinitionV1(rootNode); + } + + @Override + protected String getCommandRemoteStartNodeName() { + return "OperationStart"; + } + + @Override + protected String getCommandStopNodeName() { + return "OperationStop"; + } + + @Override + protected String getCommandWakeUpNodeName() { + return "OperationWakeUp"; + } + + @Override + protected String getDefaultCourseIdNodeName() { + return "defaultCourseId"; + } + + @Override + protected String getDryLevelNodeName() { + return "DryLevel"; + } + + @Override + protected String getNotSelectedCourseKey() { + return "0"; + } + + @Override + protected List getSupportedAPIVersions() { + return List.of(LGAPIVerion.V1_0); + } + + @Override + protected FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode, + @Nullable String targetChannelId, @Nullable String refChannelId) { + JsonNode featureNode = featuresNode.path(featureName); + if (featureNode.isMissingNode()) { + return FeatureDefinition.NULL_DEFINITION; + } + FeatureDefinition fd = new FeatureDefinition(); + fd.setName(featureName); + fd.setLabel(featureName); + fd.setChannelId(Objects.requireNonNullElse(targetChannelId, "")); + fd.setRefChannelId(Objects.requireNonNullElse(refChannelId, "")); + // All features from V1 are ENUMs + return WasherFeatureDefinition.setAllValuesMapping(fd, featureNode); + } + + @Override + public WasherDryerCapability getCapabilityInstance() { + return new WasherDryerCapability(); + } + + @Override + /* + * Return the default Course ID. + * OBS:In the V1, the default course points to the ID of the course list that is the default. + */ + protected String getDefaultCourse(JsonNode rootNode) { + return rootNode.path("Config").path("defaultCourseId").textValue(); + } + + @Override + protected String getRemoteFeatName() { + return "RemoteStart"; + } + + @Override + protected String getStandByFeatName() { + return "Standby"; + } + + @Override + protected String getConfigCourseType(JsonNode rootNode) { + if (rootNode.path(getMonitorValueNodeName()).path("APCourse").isMissingNode()) { + return "Course"; + } else { + return "APCourse"; + } + } + + @Override + protected String getCourseNodeName(JsonNode rootNode) { + JsonNode refOptions = rootNode.path(getMonitorValueNodeName()).path(getConfigCourseType(rootNode)) + .path("option"); + if (refOptions.isArray()) { + for (JsonNode node : refOptions) { + return node.asText(); + } + } + return ""; + } + + @Override + protected String getSmartCourseNodeName(JsonNode rootNode) { + return "SmartCourse"; + } + + @Override + protected String getConfigSmartCourseType(JsonNode rootNote) { + return "SmartCourse"; + } + + @Override + protected String getConfigDownloadCourseType(JsonNode rootNode) { + // just to ignore because there is no DownloadCourseType in V1 + return "XXXXXXXXXXX"; + } + + @Override + protected String getMonitorValueNodeName() { + return "Value"; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapabilityFactoryV2.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapabilityFactoryV2.java new file mode 100644 index 0000000000000..4eb0157301f77 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerCapabilityFactoryV2.java @@ -0,0 +1,284 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.CommandDefinition; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDataType; +import org.openhab.binding.lgthinq.lgservices.model.FeatureDefinition; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringResultFormat; +import org.openhab.binding.lgthinq.lgservices.model.devices.commons.washers.WasherFeatureDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ValueNode; + +/** + * The {@link WasherDryerCapabilityFactoryV2} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class WasherDryerCapabilityFactoryV2 extends AbstractWasherDryerCapabilityFactory { + private static final Logger logger = LoggerFactory.getLogger(WasherDryerCapabilityFactoryV2.class); + + @Override + protected List getSupportedAPIVersions() { + return List.of(LGAPIVerion.V2_0); + } + + @Override + public WasherDryerCapability create(JsonNode rootNode) throws LGThinqException { + WasherDryerCapability cap = super.create(rootNode); + cap.setRemoteStartFeatName("remoteStart"); + cap.setChildLockFeatName("standby"); + cap.setDoorLockFeatName("loadItemWasher"); + return cap; + } + + @Override + protected boolean hasFeatInOptions(String featName, JsonNode monitoringValueNode) { + // there's no option node in V2 + return false; + } + + @Override + protected FeatureDefinition newFeatureDefinition(String featureName, JsonNode featuresNode, + @Nullable String targetChannelId, @Nullable String refChannelId) { + JsonNode featureNode = featuresNode.path(featureName); + FeatureDefinition fd; + if ((fd = WasherFeatureDefinition.getBasicFeatureDefinition(featureName, featureNode, targetChannelId, + refChannelId)) == FeatureDefinition.NULL_DEFINITION) { + return fd; + } + JsonNode labelNode = featureNode.path("label"); + if (!labelNode.isMissingNode() && !labelNode.isNull()) { + fd.setLabel(labelNode.asText()); + } else { + fd.setLabel(featureName); + } + // all features from V2 are enums + fd.setDataType(FeatureDataType.ENUM); + JsonNode valuesMappingNode = featureNode.path("valueMapping"); + if (!valuesMappingNode.isMissingNode()) { + + Map valuesMapping = new HashMap<>(); + valuesMappingNode.fields().forEachRemaining(e -> { + // collect values as: + // + // "POWEROFF": { + // "index": 0, + // "label": "@WM_STATE_POWER_OFF_W" + // }, + // to "POWEROFF" -> "@WM_STATE_POWER_OFF_W" + valuesMapping.put(e.getKey(), e.getValue().path("label").asText()); + }); + fd.setValuesMapping(valuesMapping); + } + + return fd; + } + + @Override + public WasherDryerCapability getCapabilityInstance() { + return new WasherDryerCapability(); + } + + @Override + protected String getCourseNodeName(JsonNode rootNode) { + String courseType = getConfigCourseType(rootNode); + return rootNode.path(getMonitorValueNodeName()).path(courseType).path("ref").textValue(); + } + + @Override + protected String getSmartCourseNodeName(JsonNode rootNode) { + return "SmartCourse"; + } + + private String getConfigNodeName() { + return "Config"; + } + + @Override + /* + * Return the default Course Name + * OBS:In the V2, the default course points to the default course name + */ + protected String getDefaultCourse(JsonNode rootNode) { + return rootNode.path(getConfigNodeName()).path("defaultCourse").textValue(); + } + + @Override + protected String getRemoteFeatName() { + return "remoteStart"; + } + + @Override + protected String getStandByFeatName() { + return "standby"; + } + + @Override + protected String getConfigCourseType(JsonNode rootNode) { + return rootNode.path(getConfigNodeName()).path("courseType").textValue(); + } + + protected String getConfigSmartCourseType(JsonNode rootNode) { + return rootNode.path(getConfigNodeName()).path("smartCourseType").textValue(); + } + + protected String getConfigDownloadCourseType(JsonNode rootNode) { + return rootNode.path(getConfigNodeName()).path("downloadedCourseType").textValue(); + } + + @Override + protected String getStateFeatureNodeName() { + return "state"; + } + + @Override + protected String getProcessStateNodeName() { + return "preState"; + } + + @Override + protected String getPreStateFeatureNodeName() { + return "preState"; + } + + @Override + protected String getRinseFeatureNodeName() { + return "rinse"; + } + + @Override + protected String getTemperatureFeatureNodeName() { + return "temp"; + } + + @Override + protected String getSpinFeatureNodeName() { + return "spin"; + } + + @Override + protected String getSoilWashFeatureNodeName() { + return "soilWash"; + } + + @Override + protected String getDoorLockFeatureNodeName() { + return "doorLock"; + } + + @Override + protected MonitoringResultFormat getMonitorDataFormat(JsonNode rootNode) { + // All v2 are Json format + return MonitoringResultFormat.JSON_FORMAT; + } + + @Override + protected Map getCommandsDefinition(JsonNode rootNode) { + JsonNode commandNode = rootNode.path("ControlWifi"); + List escapeDataValues = Arrays.asList("course", "SmartCourse", "doorLock", "childLock"); + if (commandNode.isMissingNode()) { + logger.warn("No commands found in the DryerWasher definition. This is most likely a bug."); + return Collections.emptyMap(); + } + Map commands = new HashMap<>(); + for (Iterator> it = commandNode.fields(); it.hasNext();) { + Map.Entry e = it.next(); + String commandName = e.getKey(); + if ("vtCtrl".equals(commandName)) { + // ignore command + continue; + } + CommandDefinition cd = new CommandDefinition(); + JsonNode thisCommandNode = e.getValue(); + cd.setCommand(thisCommandNode.path("command").textValue()); + JsonNode dataValues = thisCommandNode.path("data").path("washerDryer"); + if (!dataValues.isMissingNode()) { + Map data = new HashMap<>(); + dataValues.fields().forEachRemaining(f -> { + // only load features outside escape. + if (!escapeDataValues.contains(f.getKey())) { + if (f.getValue().isValueNode()) { + ValueNode vn = (ValueNode) f.getValue(); + if (f.getValue().isTextual()) { + data.put(f.getKey(), vn.asText()); + } else if (f.getValue().isNumber()) { + data.put(f.getKey(), vn.asInt()); + } + } + } + }); + // add extra data features + data.put(getConfigCourseType(rootNode), ""); + data.put(getConfigSmartCourseType(rootNode), ""); + data.put("courseType", ""); + cd.setData(data); + cd.setRawCommand(thisCommandNode.toPrettyString()); + } else { + logger.warn("Data node not found in the WasherDryer definition. It's most likely a bug"); + } + commands.put(commandName, cd); + } + return commands; + } + + @Override + protected String getCommandRemoteStartNodeName() { + return "WMStart"; + } + + @Override + protected String getCommandStopNodeName() { + return "WMStop"; + } + + @Override + protected String getCommandWakeUpNodeName() { + return "WMWakeup"; + } + + @Override + protected String getDefaultCourseIdNodeName() { + return "defaultCourse"; + } + + @Override + protected String getNotSelectedCourseKey() { + return "NOT_SELECTED"; + } + + @Override + protected String getMonitorValueNodeName() { + return "MonitoringValue"; + } + + @Override + protected String getDryLevelNodeName() { + return "dryLevel"; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerSnapshot.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerSnapshot.java new file mode 100644 index 0000000000000..40195da03665e --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerSnapshot.java @@ -0,0 +1,317 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WMD_POWER_OFF_VALUE; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.model.AbstractSnapshotDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DevicePowerState; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@link WasherDryerSnapshot} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@JsonIgnoreProperties(ignoreUnknown = true) +public class WasherDryerSnapshot extends AbstractSnapshotDefinition { + private DevicePowerState powerState = DevicePowerState.DV_POWER_UNK; + private String state = ""; + private String processState = ""; + private boolean online; + private String course = ""; + private String smartCourse = ""; + private String downloadedCourse = ""; + private String temperatureLevel = ""; + private String doorLock = ""; + private String option1 = ""; + private String option2 = ""; + private String childLock = ""; + private Double remainingHour = 0.00; + private Double remainingMinute = 0.00; + private Double reserveHour = 0.00; + private Double reserveMinute = 0.00; + + private String remoteStart = ""; + private boolean remoteStartEnabled = false; + private String standByStatus = ""; + + private String dryLevel = ""; + private boolean standBy = false; + private String error = ""; + private String rinse = ""; + private String spin = ""; + + private String loadItem = ""; + + public String getLoadItem() { + return loadItem; + } + + @JsonAlias({ "LoadItem" }) + @JsonProperty("loadItemWasher") + public void setLoadItem(String loadItem) { + this.loadItem = loadItem; + } + + @JsonAlias({ "Course", "courseFL24inchBaseTitan" }) + @JsonProperty("courseFL24inchBaseTitan") + public String getCourse() { + return course; + } + + public void setCourse(String course) { + this.course = course; + } + + @JsonProperty("dryLevel") + @JsonAlias({ "DryLevel" }) + public String getDryLevel() { + return dryLevel; + } + + public void setDryLevel(String dryLevel) { + this.dryLevel = dryLevel; + } + + @JsonProperty("processState") + @JsonAlias({ "ProcessState", "preState", "PreState" }) + public String getProcessState() { + return processState; + } + + public void setProcessState(String processState) { + this.processState = processState; + } + + @JsonProperty("error") + @JsonAlias({ "Error" }) + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + @Override + public DevicePowerState getPowerStatus() { + return powerState; + } + + @Override + public void setPowerStatus(DevicePowerState value) { + this.powerState = value; + } + + @Override + public boolean isOnline() { + return online; + } + + @Override + public void setOnline(boolean online) { + this.online = online; + } + + @JsonProperty("state") + @JsonAlias({ "state", "State" }) + public String getState() { + return state; + } + + @JsonProperty("smartCourseFL24inchBaseTitan") + @JsonAlias({ "smartCourseFL24inchBaseTitan", "SmartCourse" }) + public String getSmartCourse() { + return smartCourse; + } + + @JsonProperty("downloadedCourseFL24inchBaseTitan") + @JsonAlias({ "downloadedCourseFLUpper25inchBaseUS" }) + public String getDownloadedCourse() { + return downloadedCourse; + } + + public void setDownloadedCourse(String downloadedCourse) { + this.downloadedCourse = downloadedCourse; + } + + @JsonIgnore + public String getRemainingTime() { + return String.format("%02.0f:%02.0f", getRemainingHour(), getRemainingMinute()); + } + + @JsonIgnore + public String getReserveTime() { + return String.format("%02.0f:%02.0f", getReserveHour(), getReserveMinute()); + } + + @JsonProperty("remainTimeHour") + @JsonAlias({ "remainTimeHour", "Remain_Time_H" }) + public Double getRemainingHour() { + return remainingHour; + } + + public void setRemainingHour(Double remainingHour) { + this.remainingHour = remainingHour; + } + + @JsonProperty("remainTimeMinute") + @JsonAlias({ "remainTimeMinute", "Remain_Time_M" }) + public Double getRemainingMinute() { + return remainingMinute; + } + + public void setRemainingMinute(Double remainingMinute) { + this.remainingMinute = remainingMinute; + } + + @JsonProperty("reserveTimeHour") + @JsonAlias({ "reserveTimeHour", "Reserve_Time_H" }) + public Double getReserveHour() { + return reserveHour; + } + + public void setReserveHour(Double reserveHour) { + this.reserveHour = reserveHour; + } + + @JsonProperty("reserveTimeMinute") + @JsonAlias({ "reserveTimeMinute", "Reserve_Time_M" }) + public Double getReserveMinute() { + return reserveMinute; + } + + public void setReserveMinute(Double reserveMinute) { + this.reserveMinute = reserveMinute; + } + + public void setSmartCourse(String smartCourse) { + this.smartCourse = smartCourse; + } + + @JsonProperty("temp") + @JsonAlias({ "WaterTemp" }) + public String getTemperatureLevel() { + return temperatureLevel; + } + + public void setTemperatureLevel(String temperatureLevel) { + this.temperatureLevel = temperatureLevel; + } + + @JsonProperty("doorLock") + @JsonAlias({ "DoorLock", "DoorClose" }) + public String getDoorLock() { + return doorLock; + } + + public void setDoorLock(String doorLock) { + this.doorLock = doorLock; + } + + @JsonProperty("ChildLock") + @JsonAlias({ "childLock" }) + public String getChildLock() { + return childLock; + } + + public void setChildLock(String childLock) { + this.childLock = childLock; + } + + public void setState(String state) { + this.state = state; + if (state.equals(WMD_POWER_OFF_VALUE)) { + powerState = DevicePowerState.DV_POWER_OFF; + } else { + powerState = DevicePowerState.DV_POWER_ON; + } + } + + public boolean isRemoteStartEnabled() { + return remoteStartEnabled; + } + + @JsonProperty("remoteStart") + @JsonAlias({ "RemoteStart" }) + public String getRemoteStart() { + return remoteStart; + } + + public void setRemoteStart(String remoteStart) { + this.remoteStart = remoteStart.contains("ON") || "1".equals(remoteStart) ? "ON" + : (remoteStart.contains("OFF") || "0".equals(remoteStart) ? "OFF" : remoteStart); + remoteStartEnabled = "ON".equals(this.remoteStart); + } + + @JsonProperty("standby") + @JsonAlias({ "Standby" }) + public String getStandByStatus() { + return standByStatus; + } + + public void setStandByStatus(String standByStatus) { + this.standByStatus = standByStatus.contains("ON") || "1".equals(standByStatus) ? "ON" + : (standByStatus.contains("OFF") || "0".equals(standByStatus) ? "OFF" : standByStatus); + standBy = this.standByStatus.contains("ON"); + } + + public boolean isStandBy() { + return standBy; + } + + @JsonProperty("rinse") + @JsonAlias({ "RinseOption" }) + public String getRinse() { + return rinse; + } + + public void setRinse(String rinse) { + this.rinse = rinse; + } + + @JsonProperty("spin") + @JsonAlias({ "SpinSpeed" }) + public String getSpin() { + return spin; + } + + public void setSpin(String spin) { + this.spin = spin; + } + + @JsonProperty("Option1") + public String getOption1() { + return option1; + } + + public void setOption1(String option1) { + this.option1 = option1; + } + + @JsonProperty("Option2") + public String getOption2() { + return option2; + } + + public void setOption2(String option2) { + this.option2 = option2; + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerSnapshotBuilder.java b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerSnapshotBuilder.java new file mode 100644 index 0000000000000..9abbfe56373fd --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/model/devices/washerdryer/WasherDryerSnapshotBuilder.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer; + +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.WMD_SNAPSHOT_WASHER_DRYER_NODE_V2; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqApiException; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqUnmarshallException; +import org.openhab.binding.lgthinq.lgservices.model.CapabilityDefinition; +import org.openhab.binding.lgthinq.lgservices.model.DefaultSnapshotBuilder; +import org.openhab.binding.lgthinq.lgservices.model.DeviceTypes; +import org.openhab.binding.lgthinq.lgservices.model.LGAPIVerion; +import org.openhab.binding.lgthinq.lgservices.model.MonitoringBinaryProtocol; + +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * The {@link WasherDryerSnapshotBuilder} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +public class WasherDryerSnapshotBuilder extends DefaultSnapshotBuilder { + public WasherDryerSnapshotBuilder() { + super(WasherDryerSnapshot.class); + } + + @Override + public WasherDryerSnapshot createFromBinary(String binaryData, List prot, + CapabilityDefinition capDef) throws LGThinqUnmarshallException, LGThinqApiException { + WasherDryerSnapshot snap = super.createFromBinary(binaryData, prot, capDef); + snap.setRemoteStart( + bitValue(((WasherDryerCapability) capDef).getRemoteStartFeatName(), snap.getRawData(), capDef)); + if (((WasherDryerCapability) capDef).hasDoorLook()) { + snap.setDoorLock( + bitValue(((WasherDryerCapability) capDef).getDoorLockFeatName(), snap.getRawData(), capDef)); + } + snap.setChildLock(bitValue(((WasherDryerCapability) capDef).getChildLockFeatName(), snap.getRawData(), capDef)); + return snap; + } + + @Override + protected WasherDryerSnapshot getSnapshot(Map snapMap, CapabilityDefinition capDef) { + WasherDryerSnapshot snap; + DeviceTypes type = capDef.getDeviceType(); + LGAPIVerion version = capDef.getDeviceVersion(); + switch (type) { + case DRYER_TOWER: + case DRYER: + case WASHER_TOWER: + case WASHERDRYER_MACHINE: + switch (version) { + case V1_0: { + if (type == DeviceTypes.DRYER || type == DeviceTypes.DRYER_TOWER) { + throw new IllegalArgumentException("Version 1.0 for Dryer is not supported yet."); + } else { + snap = objectMapper.convertValue(snapMap, snapClass); + snap.setRawData(snapMap); + } + } + case V2_0: { + Map washerDryerMap = Objects.requireNonNull(objectMapper + .convertValue(snapMap.get(WMD_SNAPSHOT_WASHER_DRYER_NODE_V2), new TypeReference<>() { + }), "washerDryer node must be present in the snapshot"); + snap = objectMapper.convertValue(washerDryerMap, snapClass); + setAltCourseNodeName(capDef, snap, washerDryerMap); + snap.setRawData(washerDryerMap); + return snap; + } + default: + throw new IllegalStateException("Snapshot for device type " + type + " and version " + version + + " are not supported for this builder. It most likely a bug"); + } + default: + throw new IllegalStateException( + "Snapshot for device type " + type + " not supported for this builder. It most likely a bug"); + } + } + + private static void setAltCourseNodeName(CapabilityDefinition capDef, WasherDryerSnapshot snap, + Map washerDryerMap) { + if (snap.getCourse().isEmpty() && capDef instanceof WasherDryerCapability) { + String altCourseNodeName = ((WasherDryerCapability) capDef).getDefaultCourseFieldName(); + String altSmartCourseNodeName = ((WasherDryerCapability) capDef).getDefaultSmartCourseFeatName(); + snap.setCourse(Objects.requireNonNullElse((String) washerDryerMap.get(altCourseNodeName), "")); + snap.setSmartCourse(Objects.requireNonNullElse((String) washerDryerMap.get(altSmartCourseNodeName), "")); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..dd2c29a681574 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + LG ThinQ Binding + Controlling LG ThinQ enabled devices + cloud + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/i18n/lgthinq.properties b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/i18n/lgthinq.properties new file mode 100644 index 0000000000000..8fa2789a82a05 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/i18n/lgthinq.properties @@ -0,0 +1,277 @@ +# add-on + +addon.lgthinq.name = LG ThinQ Binding +addon.lgthinq.description = Controlling LG ThinQ enabled devices + +# thing types + +thing-type.lgthinq.air-conditioner-401.label = LG ThinQ Air Conditioner +thing-type.lgthinq.air-conditioner-401.description = LG ThinQ Air Conditioner +thing-type.lgthinq.bridge.label = LGThinQ GW Bridge +thing-type.lgthinq.bridge.description = A connection to a LGThinQ Gateway +thing-type.lgthinq.dishwasher-204.label = LGThinQ Dish Washer +thing-type.lgthinq.dishwasher-204.description = LG ThinQ Dish Washer +thing-type.lgthinq.dryer-202.label = LGThinQ Dryer +thing-type.lgthinq.dryer-202.description = LG ThinQ Dryer +thing-type.lgthinq.dryer-tower-222.label = LGThinQ DryerTower +thing-type.lgthinq.dryer-tower-222.description = LG ThinQ Dryer Tower +thing-type.lgthinq.fridge-101.label = LGThinQ Fridge +thing-type.lgthinq.fridge-101.description = LG ThinQ Fridge +thing-type.lgthinq.heatpump-401HP.label = LGThinQ Heat Pump +thing-type.lgthinq.heatpump-401HP.description = LG ThinQ Heat Pump +thing-type.lgthinq.washer-201.label = LGThinQ Washer +thing-type.lgthinq.washer-201.description = LG ThinQ Washing Machine +thing-type.lgthinq.washer-tower-221.label = LGThinQ Washer Tower +thing-type.lgthinq.washer-tower-221.description = LGThinQ Washer Tower + +# thing types config + +thing-type.config.lgthinq.air-conditioner-401.group.Settings.label = Polling +thing-type.config.lgthinq.air-conditioner-401.group.Settings.description = Settings required to optimize the polling behaviour. +thing-type.config.lgthinq.air-conditioner-401.pollExtraInfoOnPowerOff.label = Extra Info +thing-type.config.lgthinq.air-conditioner-401.pollExtraInfoOnPowerOff.description = If enables, extra info will be fetched even when the device is powered off. It's not so common, since extra info are normally changed only when the device is running. +thing-type.config.lgthinq.air-conditioner-401.pollingExtraInfoPeriodSeconds.label = Polling Info Period +thing-type.config.lgthinq.air-conditioner-401.pollingExtraInfoPeriodSeconds.description = Seconds to wait to the next polling for Device's Extra Info (energy consumption, remaining filter, etc) +thing-type.config.lgthinq.air-conditioner-401.pollingPeriodPowerOffSeconds.label = Polling when off +thing-type.config.lgthinq.air-conditioner-401.pollingPeriodPowerOffSeconds.description = Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. +thing-type.config.lgthinq.air-conditioner-401.pollingPeriodPowerOnSeconds.label = Polling when on +thing-type.config.lgthinq.air-conditioner-401.pollingPeriodPowerOnSeconds.description = Seconds to wait to the next polling for device state (dashboard channels) +thing-type.config.lgthinq.bridge.alternativeServer.label = Alt Gateway Server +thing-type.config.lgthinq.bridge.alternativeServer.description = Only used for proxy/test gateway server. +thing-type.config.lgthinq.bridge.country.label = User Country +thing-type.config.lgthinq.bridge.country.description = The User Country registered in LG Account +thing-type.config.lgthinq.bridge.country.option.US = United States +thing-type.config.lgthinq.bridge.country.option.UK = United Kingdom +thing-type.config.lgthinq.bridge.country.option.BE = Belgium +thing-type.config.lgthinq.bridge.country.option.BR = Brazil +thing-type.config.lgthinq.bridge.country.option.IT = Italy +thing-type.config.lgthinq.bridge.country.option.LU = Luxembourg +thing-type.config.lgthinq.bridge.country.option.NL = Netherlands +thing-type.config.lgthinq.bridge.country.option.PL = Poland +thing-type.config.lgthinq.bridge.country.option.PT = Portugal +thing-type.config.lgthinq.bridge.country.option.DE = Germany +thing-type.config.lgthinq.bridge.country.option.DK = Denmark +thing-type.config.lgthinq.bridge.country.option.NO = Norway +thing-type.config.lgthinq.bridge.country.option.-- = Other +thing-type.config.lgthinq.bridge.language.label = User Language (LG registry) +thing-type.config.lgthinq.bridge.language.description = The User Language registered in LG Account +thing-type.config.lgthinq.bridge.language.option.en-US = American English +thing-type.config.lgthinq.bridge.language.option.nl-BE = Belgium Dutch +thing-type.config.lgthinq.bridge.language.option.en-GB = British English +thing-type.config.lgthinq.bridge.language.option.pt-BR = Brazilian Portuguese +thing-type.config.lgthinq.bridge.language.option.it-IT = Italian +thing-type.config.lgthinq.bridge.language.option.de-LU = Luxembourg German +thing-type.config.lgthinq.bridge.language.option.nl-NL = Netherlands Dutch +thing-type.config.lgthinq.bridge.language.option.pl-PL = Polish +thing-type.config.lgthinq.bridge.language.option.pt-PT = Portugal Portuguese +thing-type.config.lgthinq.bridge.language.option.de-DE = German (Standard) +thing-type.config.lgthinq.bridge.language.option.da-DK = Danish +thing-type.config.lgthinq.bridge.language.option.-- = Other +thing-type.config.lgthinq.bridge.manualCountry.label = Manual User Country +thing-type.config.lgthinq.bridge.manualCountry.description = Fill this only if selected "Other" in the Country above. Example value: "DE" +thing-type.config.lgthinq.bridge.manualLanguage.label = Manual User Lang. +thing-type.config.lgthinq.bridge.manualLanguage.description = Fill this only if selected "Other" in the Language above. Example value: de-DE +thing-type.config.lgthinq.bridge.password.label = Password +thing-type.config.lgthinq.bridge.password.description = Password from LG Thinq Personal Account +thing-type.config.lgthinq.bridge.pollingIntervalSec.label = Discovery Interval +thing-type.config.lgthinq.bridge.pollingIntervalSec.description = Polling interval to discover new devices from LG Account (in Seconds >300 or 0 disabled). +thing-type.config.lgthinq.bridge.username.label = Username +thing-type.config.lgthinq.bridge.username.description = Username from LG Thinq Personal Account +thing-type.config.lgthinq.dishwasher-204.group.Settings.label = Polling +thing-type.config.lgthinq.dishwasher-204.group.Settings.description = Settings required to optimize the polling behaviour. +thing-type.config.lgthinq.dishwasher-204.pollingPeriodPowerOffSeconds.label = Polling when off +thing-type.config.lgthinq.dishwasher-204.pollingPeriodPowerOffSeconds.description = Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. +thing-type.config.lgthinq.dishwasher-204.pollingPeriodPowerOnSeconds.label = Polling when on +thing-type.config.lgthinq.dishwasher-204.pollingPeriodPowerOnSeconds.description = Seconds to wait to the next polling for device state (dashboard channels) +thing-type.config.lgthinq.dryer-202.group.Settings.label = Polling +thing-type.config.lgthinq.dryer-202.group.Settings.description = Settings required to optimize the polling behaviour. +thing-type.config.lgthinq.dryer-202.pollingPeriodPowerOffSeconds.label = Polling when off +thing-type.config.lgthinq.dryer-202.pollingPeriodPowerOffSeconds.description = Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. +thing-type.config.lgthinq.dryer-202.pollingPeriodPowerOnSeconds.label = Polling when on +thing-type.config.lgthinq.dryer-202.pollingPeriodPowerOnSeconds.description = Seconds to wait to the next polling for device state (dashboard channels) +thing-type.config.lgthinq.dryer-tower-222.group.Settings.label = Polling +thing-type.config.lgthinq.dryer-tower-222.group.Settings.description = Settings required to optimize the polling behaviour. +thing-type.config.lgthinq.dryer-tower-222.pollingPeriodPowerOffSeconds.label = Polling when off +thing-type.config.lgthinq.dryer-tower-222.pollingPeriodPowerOffSeconds.description = Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. +thing-type.config.lgthinq.dryer-tower-222.pollingPeriodPowerOnSeconds.label = Polling when on +thing-type.config.lgthinq.dryer-tower-222.pollingPeriodPowerOnSeconds.description = Seconds to wait to the next polling for device state (dashboard channels) +thing-type.config.lgthinq.heatpump-401HP.group.Settings.label = Polling +thing-type.config.lgthinq.heatpump-401HP.group.Settings.description = Settings required to optimize the polling behaviour. +thing-type.config.lgthinq.heatpump-401HP.pollExtraInfoOnPowerOff.label = Extra Info +thing-type.config.lgthinq.heatpump-401HP.pollExtraInfoOnPowerOff.description = If enables, extra info will be fetched even when the device is powered off. It's not so common, since extra info are normally changed only when the device is running. +thing-type.config.lgthinq.heatpump-401HP.pollingExtraInfoPeriodSeconds.label = Polling Info Period +thing-type.config.lgthinq.heatpump-401HP.pollingExtraInfoPeriodSeconds.description = Seconds to wait to the next polling for Device's Extra Info (energy consumption, remaining filter, etc) +thing-type.config.lgthinq.heatpump-401HP.pollingPeriodPowerOffSeconds.label = Polling when off +thing-type.config.lgthinq.heatpump-401HP.pollingPeriodPowerOffSeconds.description = Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. +thing-type.config.lgthinq.heatpump-401HP.pollingPeriodPowerOnSeconds.label = Polling when on +thing-type.config.lgthinq.heatpump-401HP.pollingPeriodPowerOnSeconds.description = Seconds to wait to the next polling for device state (dashboard channels) +thing-type.config.lgthinq.washer-201.group.Settings.label = Polling +thing-type.config.lgthinq.washer-201.group.Settings.description = Settings required to optimize the polling behaviour. +thing-type.config.lgthinq.washer-201.pollingPeriodPowerOffSeconds.label = Polling when off +thing-type.config.lgthinq.washer-201.pollingPeriodPowerOffSeconds.description = Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. +thing-type.config.lgthinq.washer-201.pollingPeriodPowerOnSeconds.label = Polling when on +thing-type.config.lgthinq.washer-201.pollingPeriodPowerOnSeconds.description = Seconds to wait to the next polling for device state (dashboard channels) +thing-type.config.lgthinq.washer-tower-221.group.Settings.label = Polling +thing-type.config.lgthinq.washer-tower-221.group.Settings.description = Settings required to optimize the polling behaviour. +thing-type.config.lgthinq.washer-tower-221.pollingPeriodPowerOffSeconds.label = Polling when off +thing-type.config.lgthinq.washer-tower-221.pollingPeriodPowerOffSeconds.description = Seconds to wait to the next polling when device is off. Useful to save up i/o and cpu when your device is not working. If you use only this binding to control the device, you can put higher values here. +thing-type.config.lgthinq.washer-tower-221.pollingPeriodPowerOnSeconds.label = Polling when on +thing-type.config.lgthinq.washer-tower-221.pollingPeriodPowerOnSeconds.description = Seconds to wait to the next polling for device state (dashboard channels) + +# channel group types + +channel-group-type.lgthinq.ac-dashboard.label = Dashboard +channel-group-type.lgthinq.ac-dashboard.description = This is the Displayed Information. +channel-group-type.lgthinq.ac-extended-information.label = More Info +channel-group-type.lgthinq.ac-extended-information.description = Show more information about the device. +channel-group-type.lgthinq.dr-dashboard.label = Dashboard +channel-group-type.lgthinq.dr-dashboard.description = This is the Displayed Information. +channel-group-type.lgthinq.dr-remote-start-grp.label = Remote Start Options +channel-group-type.lgthinq.dr-remote-start-grp.description = Remote Start Actions and Options. +channel-group-type.lgthinq.dw-dashboard.label = Dashboard +channel-group-type.lgthinq.dw-dashboard.description = This is the Displayed Information. +channel-group-type.lgthinq.fr-dashboard.label = Dashboard +channel-group-type.lgthinq.fr-dashboard.description = This is the Displayed Information. +channel-group-type.lgthinq.fr-extended-information.label = More Info +channel-group-type.lgthinq.fr-extended-information.description = Show more information about the device. +channel-group-type.lgthinq.hp-dashboard.label = Dashboard +channel-group-type.lgthinq.hp-dashboard.description = This is the Displayed Information. +channel-group-type.lgthinq.hp-extra-information.label = More Info +channel-group-type.lgthinq.hp-extra-information.description = Show more information about the device. +channel-group-type.lgthinq.wm-dashboard.label = Dashboard +channel-group-type.lgthinq.wm-dashboard.description = This is the Displayed Information. +channel-group-type.lgthinq.wm-remote-start-grp.label = Remote Start Options +channel-group-type.lgthinq.wm-remote-start-grp.description = Remote Start Actions and Options. + +# channel types + +channel-type.lgthinq.air-clean.label = Air Clean +channel-type.lgthinq.auto-dry.label = Auto Dry +channel-type.lgthinq.cool-jet.label = Cool Jet +channel-type.lgthinq.current-energy.label = Current Energy +channel-type.lgthinq.current-energy.description = Current Energy Consumption (kWh) +channel-type.lgthinq.current-temperature.label = Temperature +channel-type.lgthinq.current-temperature.description = Current temperature. +channel-type.lgthinq.current-watts-power.label = Current Energy +channel-type.lgthinq.current-watts-power.description = Current Energy Consumption (W) +channel-type.lgthinq.dryer-child-lock.label = Child Lock +channel-type.lgthinq.dryer-child-lock.description = Dryer Child Lock +channel-type.lgthinq.dryer-child-lock.state.option.CHILDLOCK_OFF = Unlocked +channel-type.lgthinq.dryer-child-lock.state.option.CHILDLOCK_ON = Locked +channel-type.lgthinq.dryer-dry-level.label = Dry Level +channel-type.lgthinq.dryer-dry-level.description = Dryer Dry +channel-type.lgthinq.dryer-error.label = Error +channel-type.lgthinq.dryer-error.description = Dryer Error +channel-type.lgthinq.dryer-error.state.option.ERROR_TE4 = ERROR_TE4 +channel-type.lgthinq.dryer-error.state.option.ERROR_CE1 = ERROR_CE1 +channel-type.lgthinq.dryer-remain-time.label = Remaining Time +channel-type.lgthinq.dryer-remain-time.description = Dryer Remaining Time +channel-type.lgthinq.dryer-remain-time.state.pattern = %1$tH:%1$tM +channel-type.lgthinq.dryer-state.label = Dryer State +channel-type.lgthinq.dryer-state.description = Dryer Operation State +channel-type.lgthinq.energy-saving.label = Energy Saving +channel-type.lgthinq.extra-info-collector.label = Info Collector +channel-type.lgthinq.extra-info-collector.description = This switch enable collector for energy and filter consumption (if presents) +channel-type.lgthinq.fan-speed.label = Fan Speed +channel-type.lgthinq.fan-speed.description = AC Wind Strength +channel-type.lgthinq.fan-step-left-right.label = Fan HDir +channel-type.lgthinq.fan-step-left-right.description = Fan Horizontal Direction +channel-type.lgthinq.fan-step-up-down.label = Fan VDir +channel-type.lgthinq.fan-step-up-down.description = Fan Vertical Direction +channel-type.lgthinq.fr-active-saving.label = Active Saving +channel-type.lgthinq.fr-active-saving.description = Active Saving +channel-type.lgthinq.fr-eco-friendly-mode.label = Vacation +channel-type.lgthinq.fr-eco-friendly-mode.description = Vacation Mode +channel-type.lgthinq.fr-express-cool-mode.label = Express Cool +channel-type.lgthinq.fr-express-cool-mode.description = Express Cool +channel-type.lgthinq.fr-express-mode.label = Express Freeze +channel-type.lgthinq.fr-express-mode.description = Express Freeze Mode +channel-type.lgthinq.fr-fresh-air-filter.label = Fresh Air Filter +channel-type.lgthinq.fr-fresh-air-filter.description = Fresh Air Filter State. +channel-type.lgthinq.fr-ice-plus.label = Ice Plus +channel-type.lgthinq.fr-ice-plus.description = Ice Plus Feature +channel-type.lgthinq.fr-smart-saving-mode.label = Smart Saving +channel-type.lgthinq.fr-smart-saving-mode.description = Smart Saving Mode +channel-type.lgthinq.fr-smart-saving-switch.label = Smart Saving +channel-type.lgthinq.fr-smart-saving-switch.description = Smart Saving +channel-type.lgthinq.fr-water-filter.label = Water Filter +channel-type.lgthinq.fr-water-filter.description = Months passed since filter has been changed. +channel-type.lgthinq.fridge-freezer-temperature.label = Freezer Temp. +channel-type.lgthinq.fridge-freezer-temperature.description = Freezer setpoint temperature +channel-type.lgthinq.fridge-fridge-temperature.label = Fridge Setpoint Temperature +channel-type.lgthinq.fridge-fridge-temperature.description = Fridge setpoint temperature. +channel-type.lgthinq.fridge-some-door-open.label = Door Open +channel-type.lgthinq.fridge-some-door-open.description = Door status (at least one if combined fridge/freezer) +channel-type.lgthinq.fridge-some-door-open.state.option.OPEN = Open +channel-type.lgthinq.fridge-some-door-open.state.option.CLOSE = Closed +channel-type.lgthinq.fridge-temp-unit.label = Temp. Unit +channel-type.lgthinq.fridge-temp-unit.description = Temperature Unit +channel-type.lgthinq.fridge-temp-unit.state.option.CELSIUS = C +channel-type.lgthinq.fridge-temp-unit.state.option.FAHRENHEIT = F +channel-type.lgthinq.hp-air-water-switch.label = Air/Water +channel-type.lgthinq.hp-air-water-switch.description = Define the Temperature Selector based on Water/Air. +channel-type.lgthinq.hp-air-water-switch.state.option.0.0 = Air Temperature +channel-type.lgthinq.hp-air-water-switch.state.option.1.0 = Leaving Water Temperature +channel-type.lgthinq.max-temperature.label = Maximum Temp. +channel-type.lgthinq.max-temperature.description = Maximum Temperature for this mode. +channel-type.lgthinq.min-temperature.label = Minimum Temp. +channel-type.lgthinq.min-temperature.description = Minimum temperature for this mode. +channel-type.lgthinq.operation-mode.label = Operation Mode +channel-type.lgthinq.operation-mode.description = AC Operation Mode +channel-type.lgthinq.remaining-filter.label = Remaining Filter +channel-type.lgthinq.remaining-filter.description = Remaining filter without need to be replaced. +channel-type.lgthinq.rs-course.label = Course to Run +channel-type.lgthinq.rs-course.description = Course +channel-type.lgthinq.rs-rinse.label = Rinse +channel-type.lgthinq.rs-rinse.description = Rinse +channel-type.lgthinq.rs-spin.label = Spin +channel-type.lgthinq.rs-spin.description = Spin Speed +channel-type.lgthinq.rs-start-stop.label = Remote Start/Stop +channel-type.lgthinq.rs-start-stop.description = Remote Start/Stop +channel-type.lgthinq.rs-temperature-level.label = Temp. Level +channel-type.lgthinq.rs-temperature-level.description = Target Temperature Level +channel-type.lgthinq.target-temperature.label = Target Temp. +channel-type.lgthinq.target-temperature.description = Target temperature. +channel-type.lgthinq.washer-course.label = Washer Course +channel-type.lgthinq.washer-course.description = Washer Course +channel-type.lgthinq.washer-course.state.option.COTTON = Cotton +channel-type.lgthinq.washer-downloaded-course.label = Washer Download Course +channel-type.lgthinq.washer-downloaded-course.description = Washer Downloaded Course +channel-type.lgthinq.washer-downloaded-course.state.option.COTTON = Cotton +channel-type.lgthinq.washer-rinse.label = Rinse +channel-type.lgthinq.washer-rinse.description = Rinse +channel-type.lgthinq.washer-smart-course.label = Washer Smart Course +channel-type.lgthinq.washer-smart-course.description = Washer Smart Course +channel-type.lgthinq.washer-smart-course.state.option.COTTON = Cotton +channel-type.lgthinq.washer-spin.label = Spin +channel-type.lgthinq.washer-spin.description = Spin Speed +channel-type.lgthinq.washer-state.label = Washer State +channel-type.lgthinq.washer-state.description = Washer State Operation +channel-type.lgthinq.washerdryer-course.label = Course +channel-type.lgthinq.washerdryer-course.description = Course +channel-type.lgthinq.washerdryer-delay-time.label = Delay Time +channel-type.lgthinq.washerdryer-delay-time.description = Delay Time +channel-type.lgthinq.washerdryer-door-lock.label = Door Lock +channel-type.lgthinq.washerdryer-door-lock.description = Door Lock +channel-type.lgthinq.washerdryer-door-lock.state.option.DOOR_LOCK_ON = Locked +channel-type.lgthinq.washerdryer-door-lock.state.option.DOOR_LOCK_OFF = Unlocked +channel-type.lgthinq.washerdryer-process-state.label = Process State +channel-type.lgthinq.washerdryer-process-state.description = Process State +channel-type.lgthinq.washerdryer-remain-time.label = Remaining Time +channel-type.lgthinq.washerdryer-remain-time.description = Remaining Time +channel-type.lgthinq.washerdryer-remote-start.label = Remote Start +channel-type.lgthinq.washerdryer-remote-start.description = Remote start +channel-type.lgthinq.washerdryer-stand-by.label = Standby Mode +channel-type.lgthinq.washerdryer-stand-by.description = Standby Mode +channel-type.lgthinq.washerdryer-temp-level.label = Temp. Level +channel-type.lgthinq.washerdryer-temp-level.description = Target Temperature Level + +# channel types + +error.lgapi-getting-devices = Error getting device list from the account +error.toke-file-corrupted = LGThinq Bridge Token File corrupted +error.toke-refresh = Error refreshing LGThinq Bridge Token +error.toke-file-access-error = Error reading token file. +error.lgapi-communication-error = Communication Error with LG API +error.mandotory-fields-missing = Mandatory Fields Configuration Missing +offline.device-disconnected = Device is Offline diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/air-conditioner.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/air-conditioner.xml new file mode 100644 index 0000000000000..02b4792ce0e6d --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/air-conditioner.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + LG ThinQ Air Conditioner + + + + + + + + + Settings required to optimize the polling behaviour. + true + + + + Seconds to wait to the next polling when device is off. Useful to save up + i/o and cpu when your + device is + not working. If you use only this binding to control the + device, you can put higher values here. + + 10 + + + + Seconds to wait to the next polling for device state (dashboard channels) + + 10 + + + + Seconds to wait to the next polling for Device's Extra Info (energy consumption, + remaining filter, etc) + + 60 + + + + If enables, extra info will be fetched even when the device is powered off. + It's not so common, since + extra info are normally changed only when the device is running. + + false + + + + + + + This is the Displayed Information. + + + + + + + + + + + + Show more information about the device. + + + + + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/bridge.xml new file mode 100644 index 0000000000000..b8c9e0276cbd0 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/bridge.xml @@ -0,0 +1,78 @@ + + + + + + A connection to a LGThinQ Gateway + + + + + The User Language registered in LG Account + + + + + + + + + + + + + + + + + + The User Country registered in LG Account + + + + + + + + + + + + + + + + + + + Fill this only if selected "Other" in the Language above. Example value: de-DE + + + + Fill this only if selected "Other" in the Country above. Example value: "DE" + + + + Username from LG Thinq Personal Account + + + + Password from LG Thinq Personal Account + password + + + + Polling interval to discover new devices from LG Account (in Seconds >300 or 0 disabled). + 86400 + + + + Only used for proxy/test gateway server. + + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/channels.xml new file mode 100644 index 0000000000000..3f246f17e2453 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/channels.xml @@ -0,0 +1,416 @@ + + + + + Number:Temperature + + Current temperature. + Temperature + + + + + Number:Dimensionless + + Remaining filter without need to be replaced. + Battery + + + + + Number + + Define the Temperature Selector based on Water/Air. + + + + + + + + + + String + + Months passed since filter has been changed. + + + + + String + + Fresh Air Filter State. + + + + + Switch + + This switch enable collector for energy and filter consumption (if presents) + Switch + + + + Switch + + Switch + + + + Switch + + Switch + + + + Switch + + Switch + + + Switch + + Switch + + + + Number + + Fan Vertical Direction + wind + + + + + Number + + Fan Horizontal Direction + wind + + + + Number:Temperature + + Target temperature. + Temperature + + + + + Number:Temperature + + Minimum temperature for this mode. + Temperature + + + + + Number:Temperature + + Maximum Temperature for this mode. + Temperature + + + + + Number:Energy + + Current Energy Consumption (kWh) + Energy + + + + + Number:Power + + Current Energy Consumption (W) + Energy + + + + + Number + + AC Wind Strength + Wind + + + Number + + AC Operation Mode + + + + Switch + + Remote start + + + + + Switch + + Remote Start/Stop + + + + Switch + + Standby Mode + + + + String + + Rinse + + + + + String + + Rinse + + + + String + + Spin Speed + + + + + String + + Spin Speed + + + + String + + Washer State Operation + + + + + String + + Remaining Time + + + + + + String + + Delay Time + + + + + String + + Washer Course + + + + + + + + + String + + Washer Smart Course + + + + + + + + String + + Washer Downloaded Course + + + + + + + + String + + Target Temperature Level + Temperature + + + + String + + Target Temperature Level + Temperature + + + String + + Door Lock + + + + + + + + + + String + + Dryer Operation State + + + + + String + + Process State + + + + + String + + Course + + + + + String + + Course + + + + + String + + Dryer Dry + + + + + String + + Dryer Child Lock + + + + + + + + + + DateTime + + Dryer Remaining Time + + + + + String + + Dryer Error + + + + + + + + + + + Contact + + Door status (at least one if combined fridge/freezer) + + + + + + + + + String + + Temperature Unit + + + + + + + + + + Number:Temperature + + Freezer setpoint temperature + Temperature + + + + + Number:Temperature + + Fridge setpoint temperature. + Temperature + + + + + String + + Express Freeze Mode + + + + Switch + + Express Cool + + + + Switch + + Vacation Mode + + + + Switch + + Ice Plus Feature + + + + Switch + + Smart Saving + + + + String + + Smart Saving Mode + + + + Switch + + Active Saving + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/dish-washer.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/dish-washer.xml new file mode 100644 index 0000000000000..3edd477ab8428 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/dish-washer.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + LG ThinQ Dish Washer + + + + + + + Settings required to optimize the polling behaviour. + true + + + + Seconds to wait to the next polling when device is off. Useful to save up + i/o and cpu when your + device is + not working. If you use only this binding to control the + device, you can put higher values here. + + 10 + + + + Seconds to wait to the next polling for device state (dashboard channels) + + 10 + + + + + + + This is the Displayed Information. + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/dryer.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/dryer.xml new file mode 100644 index 0000000000000..2e1e7441935aa --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/dryer.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + LG ThinQ Dryer + + + + + + + + Settings required to optimize the polling behaviour. + true + + + + Seconds to wait to the next polling when device is off. Useful to save up + i/o and cpu when your + device is + not working. If you use only this binding to control the + device, you can put higher values here. + + 10 + + + + Seconds to wait to the next polling for device state (dashboard channels) + + 10 + + + + + + + + + + LG ThinQ Dryer Tower + + + + + + + + + Settings required to optimize the polling behaviour. + true + + + + Seconds to wait to the next polling when device is off. Useful to save up + i/o and cpu when your + device is + not working. If you use only this binding to control the + device, you can put higher values here. + + 10 + + + + Seconds to wait to the next polling for device state (dashboard channels) + + 10 + + + + + + + Remote Start Actions and Options. + + + + + + + + This is the Displayed Information. + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/fridge.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/fridge.xml new file mode 100644 index 0000000000000..7bffe8093ec0b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/fridge.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + LG ThinQ Fridge + + + + + + + + + + This is the Displayed Information. + + + + + + + + + + Show more information about the device. + + + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/heat-pump.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/heat-pump.xml new file mode 100644 index 0000000000000..5be5176644c67 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/heat-pump.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + LG ThinQ Heat Pump + + + + + + + + + Settings required to optimize the polling behaviour. + true + + + + Seconds to wait to the next polling when device is off. Useful to save up + i/o and cpu when your + device is + not working. If you use only this binding to control the + device, you can put higher values here. + + 10 + + + + Seconds to wait to the next polling for device state (dashboard channels) + + 10 + + + + Seconds to wait to the next polling for Device's Extra Info (energy consumption, + remaining filter, etc) + + 60 + + + + If enables, extra info will be fetched even when the device is powered off. + It's not so common, since + extra info are normally changed only when the device is running. + + false + + + + + + + This is the Displayed Information. + + + + + + + + + + + + + + Show more information about the device. + + + + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/washer-dryer.xml b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/washer-dryer.xml new file mode 100644 index 0000000000000..b86e354321f9c --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/main/resources/OH-INF/thing/washer-dryer.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + LG ThinQ Washing Machine + + + + + + + + Settings required to optimize the polling behaviour. + true + + + + Seconds to wait to the next polling when device is off. Useful to save up + i/o and cpu when your + device is + not working. If you use only this binding to control the + device, you can put higher values here. + + 10 + + + + Seconds to wait to the next polling for device state (dashboard channels) + + 10 + + + + + + + + + + LGThinQ Washer Tower + + + + + + + + Settings required to optimize the polling behaviour. + true + + + + Seconds to wait to the next polling when device is off. Useful to save up + i/o and cpu when your + device is + not working. If you use only this binding to control the + device, you can put higher values here. + + 10 + + + + Seconds to wait to the next polling for device state (dashboard channels) + + 10 + + + + + + Remote Start Actions and Options. + + + + + + + + + + + + This is the Displayed Information. + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/handler/JsonUtils.java b/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/handler/JsonUtils.java new file mode 100644 index 0000000000000..1888fa569d246 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/handler/JsonUtils.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.handler; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link JsonUtils} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +@SuppressWarnings("null") +public class JsonUtils { + public static String loadJson(String fileName) { + ClassLoader classLoader = JsonUtils.class.getClassLoader(); + try (InputStream inputStream = classLoader.getResourceAsStream(fileName)) { + if (inputStream == null) { + throw new IllegalArgumentException( + "Unexpected error. It is not expected this behaviour since json test files must be present: " + + fileName); + } + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalArgumentException( + "Unexpected error. It is not expected this behaviour since json test files must be present.", e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/handler/LGThinqBridgeTests.java b/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/handler/LGThinqBridgeTests.java new file mode 100644 index 0000000000000..24813b276c81b --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/handler/LGThinqBridgeTests.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.handler; + +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_GATEWAY_SERVICE_PATH_V2; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_OAUTH_SEARCH_KEY_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_PLATFORM_TYPE_V1; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_PLATFORM_TYPE_V2; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_PRE_LOGIN_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_LS_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_SESSION_LOGIN_PATH; +import static org.openhab.binding.lgthinq.lgservices.LGServicesConstants.LG_API_V2_USER_INFO; + +import java.io.File; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.lgthinq.internal.LGThinQBindingConstants; +import org.openhab.binding.lgthinq.internal.LGThinQBridgeConfiguration; +import org.openhab.binding.lgthinq.internal.handler.LGThinQBridgeHandler; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientService; +import org.openhab.binding.lgthinq.lgservices.LGThinQApiClientServiceFactory; +import org.openhab.binding.lgthinq.lgservices.LGThinQWMApiClientService; +import org.openhab.binding.lgthinq.lgservices.api.RestUtils; +import org.openhab.binding.lgthinq.lgservices.api.TokenManager; +import org.openhab.binding.lgthinq.lgservices.model.LGDevice; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCanonicalSnapshot; +import org.openhab.binding.lgthinq.lgservices.model.devices.ac.ACCapability; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; + +/** + * The {@link LGThinqBridgeTests} + * + * @author Nemer Daud - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@WireMockTest(httpPort = 8880) +@NonNullByDefault +@SuppressWarnings({ "unchecked", "null" }) +class LGThinqBridgeTests { + private static final Logger logger = LoggerFactory.getLogger(LGThinqBridgeTests.class); + private final String fakeBridgeName = "fakeBridgeId"; + private final String fakeLanguage = "pt-BR"; + private final String fakeCountry = "BR"; + private final String fakeUserName = "someone@some.url"; + private final String fakePassword = "somepassword"; + private final String gtwResponse = JsonUtils.loadJson("gtw-response-1.json"); + private final String preLoginResponse = JsonUtils.loadJson("prelogin-response-1.json"); + private final String userIdType = "LGE"; + private final String loginSessionId = "emp;11111111;222222222"; + private final String loginSessionResponse = String.format(JsonUtils.loadJson("login-session-response-1.json"), + loginSessionId, fakeUserName, userIdType, fakeUserName); + private final String userInfoReturned = String.format(JsonUtils.loadJson("user-info-response-1.json"), fakeUserName, + fakeUserName); + private final String dashboardListReturned = JsonUtils.loadJson("dashboard-list-response-1.json"); + private final String dashboardWMListReturned = JsonUtils.loadJson("dashboard-list-response-wm.json"); + private final String secretKey = "gregre9812012910291029120912091209"; + private final String oauthTokenSearchKeyReturned = "{\"returnData\":\"" + secretKey + "\"}"; + private final String refreshToken = "12897238974bb327862378ef290128390273aa7389723894734de"; + private final String accessToken = "11a1222c39f16a5c8b3fa45bb4c9be2e00a29a69dced2fa7fe731f1728346ee669f1a96d1f0b4925e5aa330b6dbab882772"; + private final String sessionTokenReturned = String.format(JsonUtils.loadJson("session-token-response-1.json"), + accessToken, refreshToken); + + // private String getCurrentTimestamp() { + // SimpleDateFormat sdf = new SimpleDateFormat(LG_API_DATE_FORMAT); + // sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + // return sdf.format(new Date()); + // } + + @Test + public void testDiscoveryACThings() { + setupAuthenticationMock(); + // LGThinQApiClientService service1 = + // LGThinQApiClientServiceFactory.newACApiClientService(LG_API_PLATFORM_TYPE_V1, + // mock(HttpClientFactory.class)); + LGThinQApiClientService service2 = LGThinQApiClientServiceFactory + .newACApiClientService(LG_API_PLATFORM_TYPE_V2, mock(HttpClientFactory.class)); + try { + List devices = service2.listAccountDevices("bridgeTest"); + assertEquals(devices.size(), 2); + } catch (Exception e) { + logger.error("Error testing facade", e); + } + } + + private void setupAuthenticationMock() { + stubFor(get(LG_API_GATEWAY_SERVICE_PATH_V2).willReturn(ok(gtwResponse))); + String preLoginPwd = RestUtils.getPreLoginEncPwd(fakePassword); + stubFor(post("/spx" + LG_API_PRE_LOGIN_PATH).withRequestBody(containing("user_auth2=" + preLoginPwd)) + .willReturn(ok(preLoginResponse))); + URI uri = UriBuilder.fromUri("http://localhost:8880").path("spx" + LG_API_OAUTH_SEARCH_KEY_PATH) + .queryParam("key_name", "OAUTH_SECRETKEY").queryParam("sever_type", "OP").build(); + stubFor(get(String.format("%s?%s", uri.getPath(), uri.getQuery())).willReturn(ok(oauthTokenSearchKeyReturned))); + String fakeUserNameEncoded = URLEncoder.encode(fakeUserName, StandardCharsets.UTF_8); + stubFor(post(LG_API_V2_SESSION_LOGIN_PATH + fakeUserNameEncoded) + .withRequestBody(containing("user_auth2=SOME_DUMMY_ENC_PWD")) + .withHeader("X-Signature", equalTo("SOME_DUMMY_SIGNATURE")) + .withHeader("X-Timestamp", equalTo("1643236928")).willReturn(ok(loginSessionResponse))); + stubFor(get(LG_API_V2_USER_INFO).willReturn(ok(userInfoReturned))); + stubFor(get("/v1" + LG_API_V2_LS_PATH).willReturn(ok(dashboardListReturned))); + Map empData = new LinkedHashMap<>(); + empData.put("account_type", userIdType); + empData.put("country_code", fakeCountry); + empData.put("username", fakeUserName); + + stubFor(post("/emp/oauth2/token/empsession").withRequestBody(containing("account_type=" + userIdType)) + .withRequestBody(containing("country_code=" + fakeCountry)) + .withRequestBody(containing("username=" + URLEncoder.encode(fakeUserName, StandardCharsets.UTF_8))) + .withHeader("lgemp-x-session-key", equalTo(loginSessionId)).willReturn(ok(sessionTokenReturned))); + // faking some constants + Bridge fakeThing = mock(Bridge.class); + ThingUID fakeThingUid = mock(ThingUID.class); + when(fakeThingUid.getId()).thenReturn(fakeBridgeName); + when(fakeThing.getUID()).thenReturn(fakeThingUid); + String tempDir = System.getProperty("java.io.tmpdir"); + LGThinQBindingConstants.THINQ_CONNECTION_DATA_FILE = tempDir + File.separator + "token.json"; + LGThinQBindingConstants.BASE_CAP_CONFIG_DATA_FILE = tempDir + File.separator + "thinq-cap.json"; + LGThinQBridgeHandler b = new LGThinQBridgeHandler(fakeThing, mock(HttpClientFactory.class)); + LGThinQBridgeHandler spyBridge = spy(b); + doReturn(new LGThinQBridgeConfiguration(fakeUserName, fakePassword, fakeCountry, fakeLanguage, 60, + "http://localhost:8880")).when(spyBridge).getConfigAs(any(Class.class)); + spyBridge.initialize(); + TokenManager tokenManager = new TokenManager(mock(HttpClient.class)); + try { + if (!tokenManager.isOauthTokenRegistered(fakeBridgeName)) { + tokenManager.oauthFirstRegistration(fakeBridgeName, fakeLanguage, fakeCountry, fakeUserNameEncoded, + fakePassword, ""); + } + } catch (Exception e) { + logger.error("Error testing facade", e); + } + } + + @BeforeEach + void setUp() { + String tempDir = System.getProperty("java.io.tmpdir"); + File f = new File(tempDir + File.separator + "token.json"); + f.deleteOnExit(); + } + + @Test + public void testDiscoveryWMThings() { + stubFor(get(LG_API_GATEWAY_SERVICE_PATH_V2).willReturn(ok(gtwResponse))); + String preLoginPwd = RestUtils.getPreLoginEncPwd(fakePassword); + stubFor(post("/spx" + LG_API_PRE_LOGIN_PATH).withRequestBody(containing("user_auth2=" + preLoginPwd)) + .willReturn(ok(preLoginResponse))); + URI uri = UriBuilder.fromUri("http://localhost:8880").path("spx" + LG_API_OAUTH_SEARCH_KEY_PATH) + .queryParam("key_name", "OAUTH_SECRETKEY").queryParam("sever_type", "OP").build(); + stubFor(get(String.format("%s?%s", uri.getPath(), uri.getQuery())).willReturn(ok(oauthTokenSearchKeyReturned))); + stubFor(post(LG_API_V2_SESSION_LOGIN_PATH + URLEncoder.encode(fakeUserName, StandardCharsets.UTF_8)) + .withRequestBody(containing("user_auth2=SOME_DUMMY_ENC_PWD")) + .withHeader("X-Signature", equalTo("SOME_DUMMY_SIGNATURE")) + .withHeader("X-Timestamp", equalTo("1643236928")).willReturn(ok(loginSessionResponse))); + stubFor(get(LG_API_V2_USER_INFO).willReturn(ok(userInfoReturned))); + stubFor(get("/v1" + LG_API_V2_LS_PATH).willReturn(ok(dashboardWMListReturned))); + String dataCollectedWM = JsonUtils.loadJson("wm-data-result.json"); + stubFor(get("/v1/service/devices/fakeDeviceId").willReturn(ok(dataCollectedWM))); + Map empData = new LinkedHashMap<>(); + empData.put("account_type", userIdType); + empData.put("country_code", fakeCountry); + empData.put("username", fakeUserName); + + stubFor(post("/emp/oauth2/token/empsession").withRequestBody(containing("account_type=" + userIdType)) + .withRequestBody(containing("country_code=" + fakeCountry)) + .withRequestBody(containing("username=" + URLEncoder.encode(fakeUserName, StandardCharsets.UTF_8))) + .withHeader("lgemp-x-session-key", equalTo(loginSessionId)).willReturn(ok(sessionTokenReturned))); + + // Bridge fakeThing = mock(Bridge.class); + // ThingUID fakeThingUid = mock(ThingUID.class); + // when(fakeThingUid.getId()).thenReturn(fakeBridgeName); + // when(fakeThing.getUID()).thenReturn(fakeThingUid); + String tempDir = Objects.requireNonNull(System.getProperty("java.io.tmpdir"), + "java.io.tmpdir environment variable must be set"); + LGThinQBindingConstants.THINQ_USER_DATA_FOLDER = tempDir; + LGThinQBindingConstants.THINQ_CONNECTION_DATA_FILE = tempDir + File.separator + "token.json"; + LGThinQBindingConstants.BASE_CAP_CONFIG_DATA_FILE = tempDir + File.separator + "thinq-cap.json"; + // LGThinQBridgeHandler b = new LGThinQBridgeHandler(fakeThing, mock(HttpClientFactory.class)); + + final LGThinQWMApiClientService service2 = LGThinQApiClientServiceFactory + .newWMApiClientService(LG_API_PLATFORM_TYPE_V1, mock(HttpClientFactory.class)); + TokenManager tokenManager = new TokenManager(mock(HttpClient.class)); + try { + if (!tokenManager.isOauthTokenRegistered(fakeBridgeName)) { + tokenManager.oauthFirstRegistration(fakeBridgeName, fakeLanguage, fakeCountry, fakeUserName, + fakePassword, "http://localhost:8880"); + } + List devices = service2.listAccountDevices("bridgeTest"); + assertEquals(devices.size(), 1); + // service2.getDeviceData(fakeBridgeName, "fakeDeviceId", new DishWasherCapability()); + } catch (Exception e) { + logger.error("Error testing facade", e); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityFactoryTest.java b/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityFactoryTest.java new file mode 100644 index 0000000000000..afc23d5b07a36 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/java/org/openhab/binding/lgthinq/lgservices/model/CapabilityFactoryTest.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lgthinq.lgservices.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.lgthinq.handler.JsonUtils; +import org.openhab.binding.lgthinq.lgservices.errors.LGThinqException; +import org.openhab.binding.lgthinq.lgservices.model.devices.washerdryer.WasherDryerCapability; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link CapabilityFactoryTest} + * + * @author Nemer Daud - Initial contribution + */ +@NonNullByDefault +class CapabilityFactoryTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void create() throws IOException, LGThinqException { + ClassLoader classLoader = JsonUtils.class.getClassLoader(); + assertNotNull(classLoader); + try (InputStream inputStream = classLoader.getResourceAsStream("thinq-washer-v2-cap.json")) { + assertNotNull(inputStream); + JsonNode mapper = objectMapper.readTree(inputStream); + WasherDryerCapability wpCap = CapabilityFactory.getInstance().create(mapper, WasherDryerCapability.class); + assertNotNull(wpCap); + assertEquals(40, wpCap.getCourses().size()); + assertTrue(wpCap.getRinseFeat().getValuesMapping().size() > 1); + assertTrue(wpCap.getSpinFeat().getValuesMapping().size() > 1); + assertTrue(wpCap.getSoilWash().getValuesMapping().size() > 1); + assertTrue(wpCap.getTemperatureFeat().getValuesMapping().size() > 1); + assertTrue(wpCap.hasDoorLook()); + } + } +} diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/dashboard-list-response-1.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/dashboard-list-response-1.json new file mode 100644 index 0000000000000..6c1056fbe2375 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/dashboard-list-response-1.json @@ -0,0 +1,206 @@ +{ + "resultCode":"0000", + "result":{ + "langPackCommonVer":"125.6", + "langPackCommonUri":"https://objectcontent.lgthinq.com/f1cae877-1d1e-4c12-8010-acbcdcce2df1?hdnts=exp=1706183232~hmac=257aa8146a089de87496cb13aa0b43761a19e7db225558dfb8996919746b465b", + "item":[ + { + "modelName":"RAC_056905_WW", + "subModelName":"", + "deviceType":401, + "deviceCode":"AI01", + "alias":"Bedroom", + "deviceId":"abra-cadabra-0001-5771", + "fwVer":"2.5.8_RTOS_3K", + "imageFileName":"ac_home_wall_airconditioner_img.png", + "imageUrl":"https://objectcontent.lgthinq.com/9e0177e7-0956-4284-916d-61e213f1f5ab?hdnts=exp=1702098013~hmac=e14659e3ccf369930e4cc92ca2511203037d3c258b75c627af013e4656fc49d6", + "smallImageUrl":"https://objectcontent.lgthinq.com/c7e214d7-99f0-4641-b954-f238f9d55b64?hdnts=exp=1701658820~hmac=646137b7b590866c772649d03114184628b1477eb974ca8507c0dc4ede6807c5", + "ssid":"dummy-ssid", + "macAddress":"74:40:be:92:ac:08", + "networkType":"02", + "timezoneCode":"America/Sao_Paulo", + "timezoneCodeAlias":"Brazil/Sao Paulo", + "utcOffset":-3, + "utcOffsetDisplay":"-03:00", + "dstOffset":-2, + "dstOffsetDisplay":"-02:00", + "curOffset":-2, + "curOffsetDisplay":"-02:00", + "sdsGuide":"{\"deviceCode\":\"AI01\"}", + "newRegYn":"N", + "remoteControlType":"", + "modelJsonVer":7.13, + "modelJsonUri":"https://aic.lgthinq.com:46030/api/webContents/modelJSON?modelName=modelJSON_401&countryCode=KR&contentsId=abra-cadabra-0001-5771&authKey=thinq", + "appModuleVer":12.49, + "appModuleUri":"https://objectcontent.lgthinq.com/19b24102-f2c5-4ac4-97aa-bb1abe5b4c2e?hdnts=exp=1704438018~hmac=050615be890fedc1669a632310dc837b9c6c6ebfd428ed202e2b4b19c2e05155", + "appRestartYn":"Y", + "appModuleSize":6082481, + "langPackProductTypeVer":59.9, + "langPackProductTypeUri":"https://objectcontent.lgthinq.com/5642d2e1-cb10-41b4-8e99-f1831f20afe6?hdnts=exp=1705462185~hmac=68fe0ae9ef3fd02355c87668cff6d36c2ad8c312144d7406b9c040be992a15ea", + "langPackModelVer":"", + "langPackModelUri":"", + "deviceState":"E", + "online":false, + "platformType":"thinq1", + "regDt":2.0200909053555E13, + "modelProtocol":"STANDARD", + "order":0, + "drServiceYn":"N", + "fwInfoList":[ + { + "partNumber":"SAA38690433", + "checksum":"00000000", + "verOrder":0 + } + ], + "guideTypeYn":"Y", + "guideType":"RAC_TYPE1", + "regDtUtc":"20200909073555", + "regIndex":0, + "groupableYn":"Y", + "controllableYn":"Y", + "combinedProductYn":"N", + "masterYn":"Y", + "pccModelYn":"N", + "sdsPid":{ + "sds4":"", + "sds3":"", + "sds2":"", + "sds1":"" + }, + "autoOrderYn":"N", + "modelNm":"RAC_056905_WW", + "initDevice":false, + "existsEntryPopup":"N", + "tclcount":0 + }, + { + "appType":"NUTS", + "modelCountryCode":"WW", + "countryCode":"BR", + "modelName":"RAC_056905_WW", + "deviceType":401, + "deviceCode":"AI01", + "alias":"Office", + "deviceId":"abra-cadabra-0001-5772", + "fwVer":"", + "imageFileName":"ac_home_wall_airconditioner_img.png", + "imageUrl":"https://objectcontent.lgthinq.com/9e0177e7-0956-4284-916d-61e213f1f5ab?hdnts=exp=1702098013~hmac=e14659e3ccf369930e4cc92ca2511203037d3c258b75c627af013e4656fc49d6", + "smallImageUrl":"https://objectcontent.lgthinq.com/c7e214d7-99f0-4641-b954-f238f9d55b64?hdnts=exp=1701658820~hmac=646137b7b590866c772649d03114184628b1477eb974ca8507c0dc4ede6807c5", + "ssid":"xxxxxxxxx", + "softapId":"", + "softapPass":"", + "macAddress":"", + "networkType":"02", + "timezoneCode":"America/Sao_Paulo", + "timezoneCodeAlias":"Brazil/Sao Paulo", + "utcOffset":-3, + "utcOffsetDisplay":"-03:00", + "dstOffset":-2, + "dstOffsetDisplay":"-02:00", + "curOffset":-2, + "curOffsetDisplay":"-02:00", + "sdsGuide":"{\"deviceCode\":\"AI01\"}", + "newRegYn":"N", + "remoteControlType":"", + "userNo":"xxxxxxxxxxx", + "tftYn":"N", + "modelJsonVer":12.11, + "modelJsonUri":"https://objectcontent.lgthinq.com/544a6f1c-1b10-4244-a584-d103c8519910?hdnts=exp=1706145774~hmac=bf5e96e83ffdac724b7159b8ed3d7c52f5b9a2a0ef8b67cdbbcf96b1113bd25f", + "appModuleVer":12.49, + "appModuleUri":"https://objectcontent.lgthinq.com/19b24102-f2c5-4ac4-97aa-bb1abe5b4c2e?hdnts=exp=1704438018~hmac=050615be890fedc1669a632310dc837b9c6c6ebfd428ed202e2b4b19c2e05155", + "appRestartYn":"Y", + "appModuleSize":6082481, + "langPackProductTypeVer":59.9, + "langPackProductTypeUri":"https://objectcontent.lgthinq.com/5642d2e1-cb10-41b4-8e99-f1831f20afe6?hdnts=exp=1705462185~hmac=68fe0ae9ef3fd02355c87668cff6d36c2ad8c312144d7406b9c040be992a15ea", + "deviceState":"E", + "snapshot":{ + "airState.windStrength":8.0, + "airState.wMode.lowHeating":0.0, + "airState.diagCode":0.0, + "airState.lightingState.displayControl":1.0, + "airState.wDir.hStep":0.0, + "mid":8.4615358E7, + "airState.energy.onCurrent":476.0, + "airState.wMode.airClean":0.0, + "airState.quality.sensorMon":0.0, + "airState.energy.accumulatedTime":0.0, + "airState.miscFuncState.antiBugs":0.0, + "airState.tempState.target":18.0, + "airState.operation":1.0, + "airState.wMode.jet":0.0, + "airState.wDir.vStep":2.0, + "timestamp":1.643248573766E12, + "airState.powerSave.basic":0.0, + "airState.quality.PM10":0.0, + "static":{ + "deviceType":"401", + "countryCode":"BR" + }, + "airState.quality.overall":0.0, + "airState.tempState.current":25.0, + "airState.miscFuncState.extraOp":0.0, + "airState.energy.accumulated":0.0, + "airState.reservation.sleepTime":0.0, + "airState.miscFuncState.autoDry":0.0, + "airState.reservation.targetTimeToStart":0.0, + "meta":{ + "allDeviceInfoUpdate":false, + "messageId":"fVz2AE-2SC-rf3GnerGdeQ" + }, + "airState.quality.PM1":0.0, + "airState.wMode.smartCare":0.0, + "airState.quality.PM2":0.0, + "online":true, + "airState.opMode":0.0, + "airState.reservation.targetTimeToStop":0.0, + "airState.filterMngStates.maxTime":0.0, + "airState.filterMngStates.useTime":0.0 + }, + "online":true, + "platformType":"thinq2", + "area":45883, + "regDt":2.0220111184827E13, + "blackboxYn":"Y", + "modelProtocol":"STANDARD", + "order":0, + "drServiceYn":"N", + "fwInfoList":[ + { + "checksum":"00004105", + "order":1.0, + "partNumber":"SAA40128563" + } + ], + "modemInfo":{ + "appVersion":"clip_hna_v1.9.116", + "modelName":"RAC_056905_WW", + "modemType":"QCOM_QCA4010", + "ruleEngine":"y" + }, + "guideTypeYn":"Y", + "guideType":"RAC_TYPE1", + "regDtUtc":"20220111204827", + "regIndex":0, + "groupableYn":"Y", + "controllableYn":"Y", + "combinedProductYn":"N", + "masterYn":"Y", + "pccModelYn":"N", + "sdsPid":{ + "sds4":"", + "sds3":"", + "sds2":"", + "sds1":"" + }, + "autoOrderYn":"N", + "initDevice":false, + "existsEntryPopup":"N", + "tclcount":0 + } + ], + "group":[ + + ] + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/dashboard-list-response-wm.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/dashboard-list-response-wm.json new file mode 100644 index 0000000000000..50f4050ce40ba --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/dashboard-list-response-wm.json @@ -0,0 +1,82 @@ +{ + "resultCode":"0000", + "result":{ + "langPackCommonVer":"125.6", + "langPackCommonUri":"https://objectcontent.lgthinq.com/f1cae877-1d1e-4c12-8010-acbcdcce2df1?hdnts=exp=1706183232~hmac=257aa8146a089de87496cb13aa0b43761a19e7db225558dfb8996919746b465b", + "item":[ + { + "modelName":"RAC_056905_WW", + "subModelName":"", + "deviceType":201, + "deviceCode":"WM01", + "alias":"Bedroom", + "deviceId":"washer-0001-5771", + "fwVer":"2.5.8_RTOS_3K", + "imageFileName":"washmachine-1.png", + "imageUrl":"https://objectcontent.lgthinq.com/9e0177e7-0956-4284-916d-61e213f1f5ab?hdnts=exp=1702098013~hmac=e14659e3ccf369930e4cc92ca2511203037d3c258b75c627af013e4656fc49d6", + "smallImageUrl":"https://objectcontent.lgthinq.com/c7e214d7-99f0-4641-b954-f238f9d55b64?hdnts=exp=1701658820~hmac=646137b7b590866c772649d03114184628b1477eb974ca8507c0dc4ede6807c5", + "ssid":"dummy-ssid", + "macAddress":"74:40:be:92:ac:08", + "networkType":"02", + "timezoneCode":"America/Sao_Paulo", + "timezoneCodeAlias":"Brazil/Sao Paulo", + "utcOffset":-3, + "utcOffsetDisplay":"-03:00", + "dstOffset":-2, + "dstOffsetDisplay":"-02:00", + "curOffset":-2, + "curOffsetDisplay":"-02:00", + "sdsGuide":"{\"deviceCode\":\"WM01\"}", + "newRegYn":"N", + "remoteControlType":"", + "modelJsonVer":7.13, + "modelJsonUri":"https://aic.lgthinq.com:46030/api/webContents/modelJSON?modelName=modelJSON_401&countryCode=KR&contentsId=abra-cadabra-0001-5771&authKey=thinq", + "appModuleVer":12.49, + "appModuleUri":"https://objectcontent.lgthinq.com/19b24102-f2c5-4ac4-97aa-bb1abe5b4c2e?hdnts=exp=1704438018~hmac=050615be890fedc1669a632310dc837b9c6c6ebfd428ed202e2b4b19c2e05155", + "appRestartYn":"Y", + "appModuleSize":6082481, + "langPackProductTypeVer":59.9, + "langPackProductTypeUri":"https://objectcontent.lgthinq.com/5642d2e1-cb10-41b4-8e99-f1831f20afe6?hdnts=exp=1705462185~hmac=68fe0ae9ef3fd02355c87668cff6d36c2ad8c312144d7406b9c040be992a15ea", + "langPackModelVer":"", + "langPackModelUri":"", + "deviceState":"E", + "online":false, + "platformType":"thinq2", + "regDt":2.0200909053555E13, + "modelProtocol":"STANDARD", + "order":0, + "drServiceYn":"N", + "fwInfoList":[ + { + "partNumber":"SAA38690433", + "checksum":"00000000", + "verOrder":0 + } + ], + "guideTypeYn":"Y", + "guideType":"RAC_TYPE1", + "regDtUtc":"20200909073555", + "regIndex":0, + "groupableYn":"Y", + "controllableYn":"Y", + "combinedProductYn":"N", + "masterYn":"Y", + "pccModelYn":"N", + "sdsPid":{ + "sds4":"", + "sds3":"", + "sds2":"", + "sds1":"" + }, + "autoOrderYn":"N", + "modelNm":"RAC_056905_WW", + "initDevice":false, + "existsEntryPopup":"N", + "tclcount":0 + } + ], + "group":[ + + ] + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/fridge-data-result.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/fridge-data-result.json new file mode 100644 index 0000000000000..576a4cb45b8ce --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/fridge-data-result.json @@ -0,0 +1,125 @@ +{ + "resultCode": "0000", + "result": { + "appType": "NUTS", + "modelCountryCode": "WW", + "countryCode": "NO", + "modelName": "2REB1GLTL1__2", + "deviceType": 101, + "deviceCode": "KI0104", + "alias": "My fridge", + "deviceId": "UUID", + "fwVer": "", + "imageFileName": "home_appliances_img_fridge.png", + "ssid": "WifiSSID", + "softapId": "", + "softapPass": "", + "macAddress": "", + "networkType": "02", + "timezoneCode": "Europe/Oslo", + "timezoneCodeAlias": "Europe/Oslo", + "utcOffset": 1, + "utcOffsetDisplay": "+01:00", + "dstOffset": 2, + "dstOffsetDisplay": "+02:00", + "curOffset": 1, + "curOffsetDisplay": "+01:00", + "sdsGuide": "{\"deviceCode\":\"KI01\"}", + "newRegYn": "N", + "remoteControlType": "", + "userNo": "NO00000000000", + "tftYn": "N", + "deviceState": "E", + "snapshot": { + "fwUpgradeInfo": { + "upgSched": { + "upgUtc": "0", + "cmd": "none" + } + }, + "static": { + "deviceType": "101", + "countryCode": "NO" + }, + "meta": { + "allDeviceInfoUpdate": false, + "messageId": "coDTKiAHSaGPecyqOkLRFg" + }, + "mid": 1.288660304E9, + "online": true, + "refState": { + "dispenserCapacity": 255.0, + "dispenserUnit": "IGNORE", + "freezerTemp": 255.0, + "sabbathMode": "IGNORE", + "tempUnit": "CELSIUS", + "ecoFriendly": "OFF", + "activeSaving": "IGNORE", + "voiceMode": "IGNORE", + "smartSavingRun": "IGNORE", + "atLeastOneDoorOpen": "CLOSE", + "expressMode": "IGNORE", + "freshAirFilter": "IGNORE", + "convertibleTemp": 255.0, + "waterFilter": "IGNORE", + "dispenserMode": "NOT_DEFINE_VALUE value:255", + "displayLock": "UNLOCK", + "expressFridge": "OFF", + "selfCareMode": "IGNORE", + "drawerMode": "IGNORE", + "fridgeTemp": 5.0, + "pantryMode": "IGNORE", + "craftIceMode": "IGNORE", + "dualFridgeMode": "IGNORE", + "monStatus": "NORMAL", + "smartSavingMode": "IGNORE", + "smartCareV2": "ON" + }, + "timestamp": 1.676199284675E12 + }, + "online": true, + "platformType": "thinq2", + "area": 254946, + "regDt": 2.0221216190446E13, + "blackboxYn": "Y", + "modelProtocol": "STANDARD", + "receipeVersion": 0, + "activeSaving": "IGNORE", + "smartCareV2": "ON", + "order": 0, + "nlpAlias": "none", + "drServiceYn": "N", + "fwInfoList": [ + { + "checksum": "0000AC5B", + "order": 2.0, + "partNumber": "SAA42468501" + }, + { + "checksum": "0000D2B2", + "order": 1.0, + "partNumber": "SAA42473902" + } + ], + "regDtUtc": "20221216170446", + "regIndex": 0, + "groupableYn": "N", + "controllableYn": "N", + "combinedProductYn": "N", + "masterYn": "Y", + "initDevice": false, + "firebaseLogKey": "FIREBASE:LOG:KEY:BLABLA", + "manufacture": { + "inventoryOrg": "CP3", + "macAddress": "00:00:00:00:00:00", + "manufactureModel": "GC-B411EQAF.AMCQEUR", + "manufacturedAt": "2022-06-16T01:06:21+00:00", + "registeredAt": "2022-06-27T04:18:34.022045+00:00", + "salesModel": "GLM71MCCSF.AMCQEUR", + "serialNo": "SerialNumber" + }, + "upgradableYn": "N", + "autoFwDownloadYn": "N", + "tclcount": 0 + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/fridge-data-result2.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/fridge-data-result2.json new file mode 100644 index 0000000000000..0a712df1e4daa --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/fridge-data-result2.json @@ -0,0 +1,125 @@ +{ + "resultCode": "0000", + "result": { + "appType": "NUTS", + "modelCountryCode": "WW", + "countryCode": "NO", + "modelName": "2REB1GLTL1__2", + "deviceType": 101, + "deviceCode": "KI0104", + "alias": "My fridge", + "deviceId": "UUID", + "fwVer": "", + "imageFileName": "home_appliances_img_fridge.png", + "ssid": "WifiSSID", + "softapId": "", + "softapPass": "", + "macAddress": "", + "networkType": "02", + "timezoneCode": "Europe/Oslo", + "timezoneCodeAlias": "Europe/Oslo", + "utcOffset": 1, + "utcOffsetDisplay": "+01:00", + "dstOffset": 2, + "dstOffsetDisplay": "+02:00", + "curOffset": 1, + "curOffsetDisplay": "+01:00", + "sdsGuide": "{\"deviceCode\":\"KI01\"}", + "newRegYn": "N", + "remoteControlType": "", + "userNo": "NO00000000000", + "tftYn": "N", + "deviceState": "E", + "snapshot": { + "fwUpgradeInfo": { + "upgSched": { + "upgUtc": "0", + "cmd": "none" + } + }, + "static": { + "deviceType": "101", + "countryCode": "NO" + }, + "meta": { + "allDeviceInfoUpdate": false, + "messageId": "yOfRuCbKRqyBQfll-PbaoA" + }, + "mid": 1.291752295E9, + "online": true, + "refState": { + "dispenserCapacity": 255.0, + "dispenserUnit": "IGNORE", + "freezerTemp": 255.0, + "sabbathMode": "IGNORE", + "tempUnit": "CELSIUS", + "ecoFriendly": "OFF", + "activeSaving": "IGNORE", + "voiceMode": "IGNORE", + "smartSavingRun": "IGNORE", + "atLeastOneDoorOpen": "CLOSE", + "expressMode": "IGNORE", + "freshAirFilter": "IGNORE", + "convertibleTemp": 255.0, + "waterFilter": "IGNORE", + "dispenserMode": "NOT_DEFINE_VALUE value:255", + "displayLock": "UNLOCK", + "expressFridge": "OFF", + "selfCareMode": "IGNORE", + "drawerMode": "IGNORE", + "fridgeTemp": 6.0, + "pantryMode": "IGNORE", + "craftIceMode": "IGNORE", + "dualFridgeMode": "IGNORE", + "monStatus": "NORMAL", + "smartSavingMode": "IGNORE", + "smartCareV2": "ON" + }, + "timestamp": 1.676202376697E12 + }, + "online": true, + "platformType": "thinq2", + "area": 254946, + "regDt": 2.0221216190446E13, + "blackboxYn": "Y", + "modelProtocol": "STANDARD", + "receipeVersion": 0, + "activeSaving": "IGNORE", + "smartCareV2": "ON", + "order": 0, + "nlpAlias": "none", + "drServiceYn": "N", + "fwInfoList": [ + { + "checksum": "0000AC5B", + "order": 2.0, + "partNumber": "SAA42468501" + }, + { + "checksum": "0000D2B2", + "order": 1.0, + "partNumber": "SAA42473902" + } + ], + "regDtUtc": "20221216170446", + "regIndex": 0, + "groupableYn": "N", + "controllableYn": "N", + "combinedProductYn": "N", + "masterYn": "Y", + "initDevice": false, + "firebaseLogKey": "FIREBASE:LOG:KEY:BLABLA", + "manufacture": { + "inventoryOrg": "CP3", + "macAddress": "00:00:00:00:00:00", + "manufactureModel": "GC-B411EQAF.AMCQEUR", + "manufacturedAt": "2022-06-16T01:06:21+00:00", + "registeredAt": "2022-06-27T04:18:34.022045+00:00", + "salesModel": "GLM71MCCSF.AMCQEUR", + "serialNo": "SerialNumber" + }, + "upgradableYn": "N", + "autoFwDownloadYn": "N", + "tclcount": 0 + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/gtw-response-1.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/gtw-response-1.json new file mode 100644 index 0000000000000..7125637ff95d4 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/gtw-response-1.json @@ -0,0 +1,64 @@ +{ + "resultCode":"0000", + "result":{ + "countryCode":"BR", + "languageCode":"pt-BR", + "thinq1Uri":"http://localhost:8880/api", + "thinq2Uri":"http://localhost:8880/v1", + "empUri":"http://localhost:8880", + "empSpxUri":"http://localhost:8880/spx", + "rtiUri":"localhost:8880", + "mediaUri":"localhost:8880", + "appLatestVer":"4.0.14230", + "appUpdateYn":"N", + "appLink":"market://details?id=com.lgeha.nuts", + "uuidLoginYn":"N", + "lineLoginYn":"N", + "lineChannelId":"", + "cicTel":"4004-5400", + "cicUri":"", + "isSupportVideoYn":"N", + "countryLangDescription":"Brasil/Português", + "empTermsUri":"http://localhost:8880", + "googleAssistantUri":"https://assistant.google.com/services/invoke/uid/000000d26892b8a3", + "smartWorldUri":"", + "racUri":"br.rac.lgeapi.com", + "cssUri":"https://aic-common.lgthinq.com", + "cssWebUri":"http://s3-an2-op-t20-css-web-resource.s3-website.ap-northeast-2.amazonaws.com", + "iotssUri":"https://aic-iotservice.lgthinq.com", + "chatBotUri":"", + "autoOrderSetUri":"", + "autoOrderManageUri":"", + "aiShoppingUri":"", + "onestopCall":"", + "onestopEngineerUri":"", + "hdssUri":"", + "amazonDrsYn":"N", + "features":{ + "supportTvIoTServerYn":"Y", + "disableWeatherCard":"Y", + "thinqCss":"Y", + "bleConfirmYn":"Y", + "tvRcmdContentYn":"Y", + "supportProductManualYn":"N", + "clientDbYn":"Y", + "androidAutoYn":"Y", + "searchYn":"Y", + "thinqFaq":"Y", + "thinqNotice":"Y", + "groupControlYn":"Y", + "inAppReviewYn":"Y", + "cicSupport":"Y", + "qrRegisterYn":"Y", + "supportBleYn":"Y" + }, + "serviceCards":[ + + ], + "uris":{ + "takeATourUri":"https://s3-us2-op-t20-css-contents.s3.us-west-2.amazonaws.com/workexperience-new/ios/no-version/index.html", + "gscsUri":"https://gscs-america.lge.com", + "amazonDartUri":"https://shs.lgthinq.com" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/login-session-response-1.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/login-session-response-1.json new file mode 100644 index 0000000000000..f16aa0192270d --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/login-session-response-1.json @@ -0,0 +1,55 @@ +{ + "account":{ + "loginSessionID":"%s", + "userID":"%s", + "userIDType":"%s", + "dateOfBirth":"05-05-1978", + "country":"BR", + "countryName":"Brazil", + "blacklist":"N", + "age":"43", + "isSubscribe":"N", + "changePw":"N", + "toEmailId":"N", + "periodPW":"N", + "lgAccount":"Y", + "isService":"Y", + "userNickName":"faker", + "termsList":[ + + ], + "userIDList":[ + { + "lgeIDList":[ + { + "lgeIDType":"LGE", + "userID":"%s" + } + ] + } + ], + "serviceList":[ + { + "svcCode":"SVC202", + "svcName":"LG ThinQ", + "isService":"Y", + "joinDate":"30-04-2020" + }, + { + "svcCode":"SVC710", + "svcName":"EMP OAuth", + "isService":"Y", + "joinDate":"30-04-2020" + } + ], + "displayUserID":"faker", + "notiList":{ + "totCount":"0", + "list":[ + + ] + }, + "authUser":"N", + "dummyIdFlag":"N" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/prelogin-response-1.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/prelogin-response-1.json new file mode 100644 index 0000000000000..e5162aa26e8dc --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/prelogin-response-1.json @@ -0,0 +1,5 @@ +{ + "encrypted_pw": "SOME_DUMMY_ENC_PWD", + "signature": "SOME_DUMMY_SIGNATURE", + "tStamp": "1643236928" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/session-token-response-1.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/session-token-response-1.json new file mode 100644 index 0000000000000..d7501de38a598 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/session-token-response-1.json @@ -0,0 +1,7 @@ +{ + "status": 1, + "access_token": "%s", + "expires_in": "3600", + "refresh_token": "%s", + "oauth2_backend_url": "http://localhost:8880/" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/thinq-fridge-v2-cap.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/thinq-fridge-v2-cap.json new file mode 100644 index 0000000000000..4978bb744d0e1 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/thinq-fridge-v2-cap.json @@ -0,0 +1,1347 @@ +{ + "Info": { + "productType": "REF", + "country": "WW", + "modelType": "BF", + "model": "T20_B PJT THOR (Larder)(PP/C1) 유럽", + "modelName": "2REB1GLTL1__2", + "networkType": "WIFI", + "version": "1.02" + }, + "Module": { + "WPM": { + "GRM_CEN01_Main": "201", + "GRM_CEN02_UserSaving": "202", + "GRM_CEN04_RefViewer": "202", + "GRM_CEN05_ImgViewer": "202", + "GRM_FOD01_Main": "201", + "GRM_FOD02_EditFoodInfo": "201", + "GRM_FOD03_EditFoodIcon": "201", + "GRM_FOD04_AddFood": "201", + "GRM_ENM01_Main": "201", + "GRM_ENM02_DoorOpenings": "201", + "GRM_ENM03_PowerConsume": "201", + "GRM_ENM04_SetSaving": "202", + "GRM_ECO01_Main": "202", + "GRM_ECO02_Active": "202", + "GRM_ECO03_SavingRatio": "202", + "GRM_ECO04_SavingDetail": "202", + "GRM_ECO05_ViewTip": "202", + "GCM_SDS01_SdsMain": "201", + "GRM_FOT01_Main": "201", + "GRM_SET01_Main": "201", + "GRM_SET02_PushList": "201", + "GRM_SMC01_Main": "202", + "GRM_SMC02_SafeStore": "201", + "GRM_SMC03_ActiveCooling": "201", + "GRM_SMC04_SmartFreshStorage": "201", + "GRM_SMC05_ActiveIcePlus": "201", + "GRM_PHO01_Main": "201", + "GRM_SHO01_Main": "201", + "GRM_SBS01_Main": "201", + "GRM_SBS02_Local": "201", + "GRM_SET04_WeatherLocation": "201" + }, + "Menu": [ + "GRM_SMC01_Main", + "GCM_SDS01_SdsMain", + "GRM_SET01_Main" + ] + }, + "Config": { + "targetRoot": "refState", + "ignoreValue": { + "key": "IGNORE", + "index": -99 + }, + "replaceStateValue": "@RE_STATE_REPLACE_FILTER_W", + "wifiDiagnosis": "true", + "hasInsideView": false, + "fota": "true", + "hasdoor": "Y", + "blackBox": "N", + "supportFoodManager": true, + "ecoFriendlyDefaultIndex": { + "fridgeTemp": { + "tempUnit_C": 1, + "tempUnit_F": 1 + }, + "freezerTemp": { + "tempUnit_C": 1, + "tempUnit_F": 1 + }, + "convertibleTemp": { + "tempUnit_C": 1, + "tempUnit_F": 1 + }, + "expressMode": 0, + "expressFridge": 0 + }, + "sabbathDefaultSchedule": { + "type": "location", + "startDay": "FRI", + "endDay": "SAT", + "startTime": 0, + "endTime": 0, + "weekRepeatYn": "Y" + }, + "sabbathDayListMap": { + "FRI": "@RE_TERM_DAY_FRI_W", + "SAT": "@RE_TERM_DAY_SAT_W" + }, + "smartCareVersion": "V2", + "smartCare": { + "useSmartStorage": false, + "useSmartFreshStorage": true, + "useActiveIcePlus": false, + "useActiveSavingsV2": true + }, + "sideMenuInfo": { + "GRM_FOD01_Main": { + "title": "@RE_FOOD_MANAGEMENT_W", + "image": "image/ref_sidemenu_btn_foodmanager.png" + }, + "GRM_ENM01_Main": { + "title": "@RE_ENM_TITLE_W", + "image": "image/ref_sidemenu_btn_energymonitoring.png" + }, + "GCM_SDS01_SdsMain": { + "title": "@CP_NAME_SMART_DIAGNOSIS_W", + "image": "image/ref_sidemenu_btn_smart_diagnosis.png" + }, + "GRM_SET01_Main": { + "title": "@CP_SETTING_W", + "image": "image/ref_sidemenu_btn_setting.png" + }, + "GRM_ECO01_Main": { + "title": "@RE_ENM_TITLE_W", + "image": "wpm/GRM/image/ref_sidemenu_btn_energymonitoring.png" + }, + "GRM_SMC01_Main": { + "title": "@RE_SMARTCARE_RUN_V2_W", + "image": "wpm/GRM/image/ref_sidemenu_btn_smartcare.png" + }, + "GRM_SHO01_Main": { + "title": "@RE_GROCERY_LIST_W", + "image": "wpm/GRM/image/ref_sidemenu_btn_shopping.png" + } + }, + "visibleItems": [{ + "feature": "fridgeTemp", + "imageUrl": "", + "monTitle": "@RE_TERM_FRIDGE_W", + "controlTitle": "@RE_TERM_FRIDGE_W", + "controlDisabledOption": [{ + "optionValue": "@CP_OFF_EN_W", + "replaceOptionValue": "IGNORE" + } + ] + }, { + "feature": "expressFridge", + "imageUrl": "wpm/GRM/image/ref_home_ic_coldstorage.png", + "monTitle": "@RE_TERM_EXPRESS_FRIDGE_W", + "controlTitle": "@RE_TERM_EXPRESS_FRIDGE_W", + "templateType": "typeSwitch.html" + }, { + "feature": "ecoFriendly", + "imageUrl": "image/icon_fridge_eco.png", + "monTitle": "@RE_TERM_ECO_FRIENDLY_W", + "controlTitle": "@RE_TERM_ECO_FRIENDLY_W", + "templateType": "typeSwitch.html" + }, { + "feature": "smartCareV2", + "imageUrl": "wpm/GRM/image/ref_home_ic_smartcare.png", + "monTitle": "@RE_SMARTCARE_RUN_V2_W", + "controlTitle": "@RE_SMARTCARE_RUN_V2_W", + "templateType": "NONE" + } + ] + }, + "MonitoringValue": { + "monStatus": { + "_comment": "Monitoring Status _ Not Shown Item", + "dataType": "enum", + "default": "NORMAL", + "visibleItem": { + "monitoringIndex": [ + 0, + 1, + 2 + ], + "controlIndex": [ + 0, + 1, + 2 + ] + }, + "valueMapping": { + "FAIL": { + "index": 0, + "label": "", + "_comment": "Fail" + }, + "NOT_WORK": { + "index": 1, + "label": "", + "_comment": "Not working" + }, + "NORMAL": { + "index": 2, + "label": "", + "_comment": "Normal" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "fridgeTemp": { + "_comment": "Fridge Target Temperature", + "dataType": "range", + "default": 1, + "visibleItem": { + "monitoringIndex": [], + "controlIndex": [] + }, + "targetKey": { + "tempUnit": { + "CELSIUS": "fridgeTemp_C", + "FAHRENHEIT": "fridgeTemp_F" + } + }, + "valueMapping": { + "min": 0, + "max": 255, + "step": 1 + } + }, + "freezerTemp": { + "_comment": "Freezer Target Temperature", + "dataType": "range", + "default": 1, + "visibleItem": { + "monitoringIndex": [], + "controlIndex": [] + }, + "targetKey": { + "tempUnit": { + "CELSIUS": "freezerTemp_C", + "FAHRENHEIT": "freezerTemp_F" + } + }, + "valueMapping": { + "min": 0, + "max": 255, + "step": 1 + } + }, + "convertibleTemp": { + "_comment": "Convertible Target Temperature", + "dataType": "range", + "default": 1, + "visibleItem": { + "monitoringIndex": [], + "controlIndex": [] + }, + "targetKey": { + "tempUnit": { + "CELSIUS": "convertibleTemp_C", + "FAHRENHEIT": "convertibleTemp_F" + } + }, + "valueMapping": { + "min": 0, + "max": 255, + "step": 1 + } + }, + "expressMode": { + "_comment": "Express Fridge, ExpressFreeze, Rapid Freeze", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [ + 0, + 1 + ] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Express Mode OFF" + }, + "EXPRESS_ON": { + "index": 1, + "label": "@CP_ON_EN_W", + "_comment": "Express Fridge or Express Freeze ON" + }, + "RAPID_ON": { + "index": 2, + "label": "@RE_MAIN_SPEED_FREEZE_TERM_W", + "_comment": "Rapid Freeze ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "tempUnit": { + "_comment": "Temperature Unit", + "dataType": "enum", + "default": "FAHRENHEIT", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [] + }, + "valueMapping": { + "CELSIUS": { + "index": 0, + "label": "˚C", + "_comment": "Celsius Unit" + }, + "FAHRENHEIT": { + "index": 1, + "label": "˚F", + "_comment": "Fahrenheit Unit" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "freshAirFilter": { + "_comment": "Fresh Air Filter Status", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 1, + 2, + 3 + ], + "controlIndex": [] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_TERM_OFF_KO_W", + "_comment": "Fresh Air Filter OFF" + }, + "AUTO": { + "index": 1, + "label": "@RE_STATE_FRESH_AIR_FILTER_MODE_AUTO_W", + "_comment": "Fresh Air Filter AUTO" + }, + "POWER": { + "index": 2, + "label": "@RE_STATE_FRESH_AIR_FILTER_MODE_POWER_W", + "_comment": "Fresh Air Filter POWER" + }, + "REPLACE": { + "index": 3, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Fresh Air Filter REPLACE" + }, + "SMART_STORAGE_POWER": { + "index": 4, + "label": "", + "_comment": "Fresh Air Filter Smart Storage POWER" + }, + "SMART_STORAGE_OFF": { + "index": 5, + "label": "", + "_comment": "Fresh Air Filter Smart Storage OFF" + }, + "SMART_STORAGE_ON": { + "index": 6, + "label": "", + "_comment": "Fresh Air Filter Smart Storage ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "waterFilter": { + "_comment": "Water Filter Status", + "dataType": "enum", + "default": "0_MONTH", + "visibleItem": { + "monitoringIndex": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "controlIndex": [] + }, + "valueMapping": { + "0_MONTH": { + "index": 0, + "label": "@RE_TERM_OK_W", + "_comment": "Water Filter 0 Month Passed" + }, + "1_MONTH": { + "index": 1, + "label": "@RE_TERM_OK_W", + "_comment": "Water Filter 1 Month Passed" + }, + "2_MONTH": { + "index": 2, + "label": "@RE_TERM_OK_W", + "_comment": "Water Filter 2 Month Passed" + }, + "3_MONTH": { + "index": 3, + "label": "@RE_TERM_OK_W", + "_comment": "Water Filter 3 Month Passed" + }, + "4_MONTH": { + "index": 4, + "label": "@RE_TERM_OK_W", + "_comment": "Water Filter 4 Month Passed" + }, + "5_MONTH": { + "index": 5, + "label": "@RE_TERM_OK_W", + "_comment": "Water Filter 5 Month Passed" + }, + "6_MONTH": { + "index": 6, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Water Filter 6 Month Passed" + }, + "7_MONTH": { + "index": 7, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Water Filter 7 Month Passed" + }, + "8_MONTH": { + "index": 8, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Water Filter 8 Month Passed" + }, + "9_MONTH": { + "index": 9, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Water Filter 9 Month Passed" + }, + "10_MONTH": { + "index": 10, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Water Filter 10 Month Passed" + }, + "11_MONTH": { + "index": 11, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Water Filter 11 Month Passed" + }, + "12_MONTH": { + "index": 12, + "label": "@RE_STATE_REPLACE_FILTER_W", + "_comment": "Water Filter 12 Month Passed" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "displayLock": { + "_comment": "Display Lock Status(unlock, lock)", + "dataType": "enum", + "default": "UNLOCK", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [] + }, + "valueMapping": { + "UNLOCK": { + "index": 0, + "label": "", + "_comment": "Display Panel Unlocked" + }, + "LOCK": { + "index": 1, + "label": "", + "_comment": "Display Panel Locked" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "sabbathMode": { + "_comment": "Sabbath Mode State (ON, OFF)", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Sabbath Mode OFF" + }, + "ON": { + "index": 1, + "label": "@CP_ON_EN_W", + "_comment": "Sabbath Mode ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "atLeastOneDoorOpen": { + "_comment": "Door Open State(Close or Open) global", + "dataType": "enum", + "default": "CLOSE", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [] + }, + "valueMapping": { + "CLOSE": { + "index": 0, + "label": "", + "_comment": "Door Closed" + }, + "OPEN": { + "index": 1, + "label": "", + "_comment": "Door Open" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "smartSavingMode": { + "_comment": "Smart Saving Setting Status", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 3, + 4 + ], + "controlIndex": [] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Smart Saving OFF" + }, + "NIGHT_ON": { + "index": 1, + "label": "@RE_SMARTSAVING_MODE_NIGHT_W", + "_comment": "Smart Saving Night Mode ON" + }, + "CUSTOM_ON": { + "index": 2, + "label": "@RE_SMARTSAVING_MODE_CUSTOM_W", + "_comment": "Smart Saving Custom Mode ON" + }, + "SMARTGRID_DR_ON": { + "index": 3, + "label": "@RE_TERM_DEMAND_RESPONSE_FUNCTIONALITY_W", + "_comment": "Smart Grid Demand Response Mode ON" + }, + "SMARTGRID_DD_ON": { + "index": 4, + "label": "@RE_TERM_DELAY_DEFROST_CAPABILITY_W", + "_comment": "Smart Grid Delay Defrost Mode ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "smartSavingRun": { + "_comment": "Smart Saving Running Status", + "dataType": "enum", + "default": "STOP", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [] + }, + "valueMapping": { + "STOP": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Smart Saving Stop (Smart Grid)" + }, + "RUN": { + "index": 1, + "label": "@CP_ON_EN_W", + "_comment": "Smart Saving Running (Smart Grid)" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "activeSaving": { + "_comment": "Active Saving Status", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Active Saving OFF" + }, + "ON": { + "index": 1, + "label": "@CP_ON_EN_W", + "_comment": "Active Saving ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "ecoFriendly": { + "_comment": "Eco Friendly Status", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [ + 0, + 1 + ] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Eco Friendly OFF" + }, + "ON": { + "index": 1, + "label": "@CP_ON_EN_W", + "_comment": "Eco Friendly ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "fridgeTemp_C": { + "dataType": "enum", + "default": "1", + "_comment": "Temperature Unit :℉ or ℃ ", + "visibleItem": { + "monitoringIndex": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "controlIndex": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "valueMapping": { + "1": { + "index": 1, + "label": "7", + "_comment": "" + }, + "2": { + "index": 2, + "label": "6", + "_comment": "" + }, + "3": { + "index": 3, + "label": "5", + "_comment": "" + }, + "4": { + "index": 4, + "label": "4", + "_comment": "" + }, + "5": { + "index": 5, + "label": "3", + "_comment": "" + }, + "6": { + "index": 6, + "label": "2", + "_comment": "" + }, + "7": { + "index": 7, + "label": "1", + "_comment": "" + }, + "255": { + "index": 255, + "label": "IGNORE", + "_comment": "" + } + } + }, + "fridgeTemp_F": { + "dataType": "enum", + "default": "1", + "_comment": "Temperature Unit :℉ or ℃ ", + "visibleItem": { + "monitoringIndex": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ], + "controlIndex": [] + }, + "valueMapping": { + "1": { + "index": 1, + "label": "46", + "_comment": "" + }, + "2": { + "index": 2, + "label": "45", + "_comment": "" + }, + "3": { + "index": 3, + "label": "44", + "_comment": "" + }, + "4": { + "index": 4, + "label": "43", + "_comment": "" + }, + "5": { + "index": 5, + "label": "42", + "_comment": "" + }, + "6": { + "index": 6, + "label": "41", + "_comment": "" + }, + "7": { + "index": 7, + "label": "40", + "_comment": "" + }, + "8": { + "index": 8, + "label": "39", + "_comment": "" + }, + "9": { + "index": 9, + "label": "38", + "_comment": "" + }, + "10": { + "index": 10, + "label": "37", + "_comment": "" + }, + "11": { + "index": 11, + "label": "36", + "_comment": "" + }, + "12": { + "index": 12, + "label": "35", + "_comment": "" + }, + "13": { + "index": 13, + "label": "34", + "_comment": "" + }, + "14": { + "index": 14, + "label": "33", + "_comment": "" + }, + "255": { + "index": 255, + "label": "IGNORE", + "_comment": "" + } + } + }, + "freezerTemp_C": { + "dataType": "enum", + "default": "1", + "_comment": "Temperature Unit :℉ or ℃ ", + "visibleItem": { + "monitoringIndex": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "controlIndex": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + }, + "valueMapping": { + "1": { + "index": 1, + "label": "-15", + "_comment": "" + }, + "2": { + "index": 2, + "label": "-16", + "_comment": "" + }, + "3": { + "index": 3, + "label": "-17", + "_comment": "" + }, + "4": { + "index": 4, + "label": "-18", + "_comment": "" + }, + "5": { + "index": 5, + "label": "-19", + "_comment": "" + }, + "6": { + "index": 6, + "label": "-20", + "_comment": "" + }, + "7": { + "index": 7, + "label": "-21", + "_comment": "" + }, + "8": { + "index": 8, + "label": "-22", + "_comment": "" + }, + "9": { + "index": 9, + "label": "-23", + "_comment": "" + }, + "255": { + "index": 255, + "label": "IGNORE", + "_comment": "" + } + } + }, + "freezerTemp_F": { + "dataType": "enum", + "default": "1", + "_comment": "Temperature Unit :℉ or ℃ ", + "visibleItem": { + "monitoringIndex": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + "controlIndex": [] + }, + "valueMapping": { + "0": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "" + }, + "1": { + "index": 1, + "label": "7", + "_comment": "" + }, + "2": { + "index": 2, + "label": "6", + "_comment": "" + }, + "3": { + "index": 3, + "label": "5", + "_comment": "" + }, + "4": { + "index": 4, + "label": "4", + "_comment": "" + }, + "5": { + "index": 5, + "label": "3", + "_comment": "" + }, + "6": { + "index": 6, + "label": "2", + "_comment": "" + }, + "7": { + "index": 7, + "label": "1", + "_comment": "" + }, + "8": { + "index": 8, + "label": "0", + "_comment": "" + }, + "9": { + "index": 9, + "label": "-1", + "_comment": "" + }, + "10": { + "index": 10, + "label": "-2", + "_comment": "" + }, + "11": { + "index": 11, + "label": "-3", + "_comment": "" + }, + "12": { + "index": 12, + "label": "-7", + "_comment": "" + }, + "13": { + "index": 13, + "label": "-12", + "_comment": "" + }, + "14": { + "index": 14, + "label": "-15", + "_comment": "" + }, + "15": { + "index": 15, + "label": "-17", + "_comment": "" + }, + "255": { + "index": 255, + "label": "IGNORE", + "_comment": "" + } + } + }, + "convertibleTemp_C": { + "dataType": "enum", + "default": "1", + "_comment": "Temperature Unit :℉ or ℃ ", + "visibleItem": { + "monitoringIndex": [ + 0, + 1, + 2, + 3, + 5, + 7, + 9, + 11, + 12, + 13 + ], + "controlIndex": [] + }, + "valueMapping": { + "0": { + "index": 0, + "label": "-13", + "_comment": "" + }, + "1": { + "index": 1, + "label": "-13", + "_comment": "" + }, + "2": { + "index": 2, + "label": "-14", + "_comment": "" + }, + "3": { + "index": 3, + "label": "-15", + "_comment": "" + }, + "5": { + "index": 5, + "label": "-16", + "_comment": "" + }, + "7": { + "index": 7, + "label": "-17", + "_comment": "" + }, + "9": { + "index": 9, + "label": "-18", + "_comment": "" + }, + "11": { + "index": 11, + "label": "-19", + "_comment": "" + }, + "12": { + "index": 12, + "label": "-20", + "_comment": "" + }, + "13": { + "index": 13, + "label": "-21", + "_comment": "" + }, + "255": { + "index": 255, + "label": "IGNORE", + "_comment": "" + } + } + }, + "convertibleTemp_F": { + "dataType": "enum", + "default": "1", + "_comment": "Temperature Unit :℉ or ℃ ", + "visibleItem": { + "monitoringIndex": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13 + ], + "controlIndex": [] + }, + "valueMapping": { + "0": { + "index": 0, + "label": "8", + "_comment": "" + }, + "1": { + "index": 1, + "label": "8", + "_comment": "" + }, + "2": { + "index": 2, + "label": "6", + "_comment": "" + }, + "3": { + "index": 3, + "label": "5", + "_comment": "" + }, + "4": { + "index": 4, + "label": "4", + "_comment": "" + }, + "5": { + "index": 5, + "label": "3", + "_comment": "" + }, + "6": { + "index": 6, + "label": "2", + "_comment": "" + }, + "7": { + "index": 7, + "label": "1", + "_comment": "" + }, + "8": { + "index": 8, + "label": "0", + "_comment": "" + }, + "9": { + "index": 9, + "label": "-1", + "_comment": "" + }, + "10": { + "index": 10, + "label": "-2", + "_comment": "" + }, + "11": { + "index": 11, + "label": "-3", + "_comment": "" + }, + "12": { + "index": 12, + "label": "-4", + "_comment": "" + }, + "13": { + "index": 13, + "label": "-6", + "_comment": "" + }, + "255": { + "index": 255, + "label": "IGNORE", + "_comment": "" + } + } + }, + "smartSavingModeCustomOpt": { + "dataType": "string" + }, + "smartCareV2": { + "_comment": "Smart CareV2 State (ON, OFF)", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [ + 0, + 1 + ] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Smart CareV2 OFF" + }, + "ON": { + "index": 1, + "label": "@CP_ON_EN_W", + "_comment": "Smart CareV2 ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + }, + "expressFridge": { + "_comment": "Express Fridge Status", + "dataType": "enum", + "default": "OFF", + "visibleItem": { + "monitoringIndex": [ + 0, + 1 + ], + "controlIndex": [ + 0, + 1 + ] + }, + "valueMapping": { + "OFF": { + "index": 0, + "label": "@CP_OFF_EN_W", + "_comment": "Express Fridge OFF" + }, + "ON": { + "index": 1, + "label": "@CP_ON_EN_W", + "_comment": "Express Fridge ON" + }, + "IGNORE": { + "index": 255, + "label": "", + "_comment": "Please ignore me" + } + } + } + }, + "ControlWifi": { + "basicCtrl": { + "command": "Set", + "data": { + "refState": { + "fridgeTemp": "{{fridgeTemp}}", + "fridgeDoorOpen": "{{fridgeDoorOpen}}", + "freezerTemp": "{{freezerTemp}}", + "freezerDoorOpen": "{{freezerDoorOpen}}", + "convertibleTemp": "{{convertibleTemp}}", + "convertibleDoorOpen": "{{convertibleDoorOpen}}", + "didDoorOpen": "{{didDoorOpen}}", + "smartSavingMode": "{{smartSavingMode}}", + "smartSavingRun": "{{smartSavingRun}}", + "activeSaving": "{{activeSaving}}", + "ecoFriendly": "{{ecoFriendly}}", + "expressMode": "{{expressMode}}", + "tempUnit": "{{tempUnit}}", + "freshAirFilter": "{{freshAirFilter}}", + "waterFilter": "{{waterFilter}}", + "displayLock": "{{displayLock}}", + "sabbathMode": "{{sabbathMode}}", + "atLeastOneDoorOpen": "{{atLeastOneDoorOpen}}", + "expressFridge": "{{expressFridge}}" + } + } + }, + "getActiveSavingScheduleCtrl": { + "command": "Get", + "data": {} + }, + "getSmartFreshStorageScheduleCtrl": { + "command": "Get", + "data": {} + }, + "getActiveIcePlusScheduleCtrl": { + "command": "Get", + "data": {} + } + }, + "Push": [{ + "category": "PUSH_REF_STATE", + "label": "@RE_SETTING_PUSH_PRODUCT_STATE_W", + "groupCode": "10101" + } + ], + "SmartMode": {} +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/thinq-washer-v2-cap.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/thinq-washer-v2-cap.json new file mode 100644 index 0000000000000..044f72c1c89a5 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/thinq-washer-v2-cap.json @@ -0,0 +1,3235 @@ +{ + "Info": { + "productType": "WM", + "country": "WW", + "modelType": "FL", + "MP Project": "Vivace", + "ProjectName": "Vivace WO VC3 550 V700 1600 rpm ", + "modelName": "F_V8_Y___W.B_2QEUK", + "networkType": "WIFI", + "version": "0.8" + }, + "Module": { + "WPM": { + "GWM_CEN01_Main": "201", + "GWM_CRS01_Main": "201", + "GWM_CRS02_CourseList": "201", + "GWM_CRS03_CourseDetail": "201", + "GWM_WCH01_Main": "201", + "GWM_WCH01_UserGuide2": "201", + "GWM_ENM01_Main": "201", + "GCM_SDS01_SdsMain": "001", + "GWM_SET01_Main": "201", + "GWM_SET02_PushList": "201", + "GWM_FOT01_Main": "201" + }, + "Menu": [ + "GWM_CRS01_Main", + "GWM_WCH01_Main", + "GWM_ENM01_Main", + "GCM_SDS01_SdsMain", + "GWM_SET01_Main" + ] + }, + "Config": { + "downloadPanelLabel": "@WM_TERM_DOWNLOAD_CYCLE_EN_W", + "remoteStartLabel": "@WM_TITAN2_OPTION_REMOTE_START_W", + "maxDownloadCourseNum": 1, + "defaultCourse": "COTTON", + "downloadCourseAPId": "RINSESPIN", + "defaultSmartCourse": "RINSESPIN", + "tubCleanCourseId": "TUB_CLEAN", + "standbyEnable": true, + "fota": true, + "powerOffDownload": true, + "expectedStartTime": false, + "isAIDDAvailable": true, + "SmartCourseCategory": [ + { + "label": "@WM_COURSE_CATEGORY_HOME_FAMILY_W", + "courseIdList": [ + "BABYCARE", + "HYGIENE", + "SMALLLOAD", + "SKINCARE", + "RINSESPIN", + "KIDSWEAR", + "SCHOOLUNIFORM", + "RAINYSEASON", + "SINGLEGARMENT", + "NOISEMINIMIZE", + "MINIMIZEWRINKLES", + "LIGHTLYSOILEDITEMS", + "MINIMIZEDETERGENT", + "SLEEVEHEMSANDCOLLARS", + "JUICEANDFOODSTAINS", + "QUICKTUBCLEAN", + "DRAIN", + "SPIN" + ] + }, + { + "label": "@WM_COURSE_CATEGORY_SPORTS_LEISURE_W", + "courseIdList": [ + "SWIMMINGWEAR", + "GYMCLOTHES", + "SWEATSTAIN" + ] + }, + { + "label": "@WM_COURSE_CATEGORY_FABRIC_CARE_W", + "courseIdList": [ + "LINGERIE", + "COLDWASH", + "JEANS", + "BLANKET", + "COLORPROTECTION" + ] + } + ], + "smartCourseType": "smartCourseFL24inchBaseTitan", + "courseType": "courseFL24inchBaseTitan", + "downloadedCourseType": "downloadedCourseFL24inchBaseTitan" + }, + "MonitoringValue": { + "state": { + "dataType": "enum", + "label": null, + "valueMapping": { + "POWEROFF": { + "index": 0, + "label": "@WM_STATE_POWER_OFF_W" + }, + "INITIAL": { + "index": 1, + "label": "@WM_STATE_INITIAL_W" + }, + "PAUSE": { + "index": 2, + "label": "@WM_STATE_PAUSE_W" + }, + "RESERVED": { + "index": 3, + "label": "@WM_STATE_RESERVE_W" + }, + "DETECTING": { + "index": 4, + "label": "@WM_STATE_DETECTING_W" + }, + "RUNNING": { + "index": 6, + "label": "@WM_STATE_RUNNING_W" + }, + "RINSING": { + "index": 7, + "label": "@WM_STATE_RINSING_W" + }, + "SPINNING": { + "index": 8, + "label": "@WM_STATE_SPINNING_W" + }, + "DRYING": { + "index": 9, + "label": "@WM_STATE_DRYING_W" + }, + "END": { + "index": 10, + "label": "@WM_STATE_END_W" + }, + "COOLDOWN": { + "index": 11, + "label": "@WM_STATE_COOLDOWN_W" + }, + "RINSEHOLD": { + "index": 12, + "label": "@WM_STATE_RINSEHOLD_W" + }, + "WASH_REFRESHING": { + "index": 14, + "label": "@WM_STATE_WASH_REFRESHING_W" + }, + "STEAMSOFTENING": { + "index": 15, + "label": "@WM_STATE_STEAMSOFTENING_W" + }, + "DEMO": { + "index": 16, + "label": "@WM_STATE_DEMO_W" + }, + "ERROR": { + "index": 18, + "label": "@WM_STATE_ERROR_W" + } + } + }, + "preState": { + "dataType": "enum", + "label": null, + "valueMapping": { + "POWEROFF": { + "index": 0, + "label": "@WM_STATE_POWER_OFF_W" + }, + "INITIAL": { + "index": 1, + "label": "@WM_STATE_INITIAL_W" + }, + "PAUSE": { + "index": 2, + "label": "@WM_STATE_PAUSE_W" + }, + "RESERVED": { + "index": 3, + "label": "@WM_STATE_RESERVE_W" + }, + "DETECTING": { + "index": 4, + "label": "@WM_STATE_DETECTING_W" + }, + "RUNNING": { + "index": 6, + "label": "@WM_STATE_RUNNING_W" + }, + "RINSING": { + "index": 7, + "label": "@WM_STATE_RINSING_W" + }, + "SPINNING": { + "index": 8, + "label": "@WM_STATE_SPINNING_W" + }, + "DRYING": { + "index": 9, + "label": "@WM_STATE_DRYING_W" + }, + "END": { + "index": 10, + "label": "@WM_STATE_END_W" + }, + "COOLDOWN": { + "index": 11, + "label": "@WM_STATE_COOLDOWN_W" + }, + "RINSEHOLD": { + "index": 12, + "label": "@WM_STATE_RINSEHOLD_W" + }, + "WASH_REFRESHING": { + "index": 14, + "label": "@WM_STATE_WASH_REFRESHING_W" + }, + "STEAMSOFTENING": { + "index": 15, + "label": "@WM_STATE_STEAMSOFTENING_W" + }, + "DEMO": { + "index": 16, + "label": "@WM_STATE_DEMO_W" + }, + "ERROR": { + "index": 18, + "label": "@WM_STATE_ERROR_W" + } + } + }, + "remoteStart": { + "dataType": "enum", + "label": "@WM_OPTION_REMOTE_START_W", + "valueMapping": { + "REMOTE_START_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "REMOTE_START_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "initialBit": { + "dataType": "enum", + "label": null, + "valueMapping": { + "INITIAL_BIT_OFF": { + "index": 0, + "label": "INITIAL_BIT_OFF" + }, + "INITIAL_BIT_ON": { + "index": 1, + "label": "INITIAL_BIT_ON" + } + } + }, + "AIDDLed": { + "dataType": "enum", + "valueMapping": { + "AIDDLed_OFF": { + "index": 0, + "label": "AIDDLed_OFF" + }, + "AIDDLed_ON": { + "index": 1, + "label": "AIDDLed_ON" + } + } + }, + "childLock": { + "dataType": "enum", + "label": "@WM_OPTION_CHILDLOCK_W", + "valueMapping": { + "CHILDLOCK_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "CHILDLOCK_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "TCLCount": { + "dataType": "range", + "label": null, + "valueMapping": { + "min": 0, + "max": 60 + } + }, + "reserveTimeHour": { + "dataType": "range", + "label": "@WM_TITAN2_OPTION_DELAY_END_W", + "valueMapping": { + "min": 3, + "max": 19 + } + }, + "reserveTimeMinute": { + "dataType": "range", + "label": null, + "valueMapping": { + "min": 0, + "max": 59 + } + }, + "remainTimeHour": { + "dataType": "range", + "label": null, + "valueMapping": { + "min": 0, + "max": 30 + } + }, + "remainTimeMinute": { + "dataType": "range", + "label": null, + "valueMapping": { + "min": 0, + "max": 59 + } + }, + "initialTimeHour": { + "dataType": "range", + "label": null, + "valueMapping": { + "min": 0, + "max": 30 + } + }, + "initialTimeMinute": { + "dataType": "range", + "label": null, + "valueMapping": { + "min": 0, + "max": 59 + } + }, + "soilWash": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_WASH_W", + "valueMapping": { + "NO_SOILWASH": { + "index": 0, + "label": "@WM_TERM_NO_SELECT_W" + }, + "SOILWASH_TURBO_WASH": { + "index": 1, + "label": "@WM_TITAN2_OPTION_TURBO_WASH_W" + }, + "SOILWASH_TIMESAVE": { + "index": 2, + "label": "@WM_TITAN2_OPTION_WASH_TIMESAVE_W" + }, + "SOILWASH_NORMAL": { + "index": 3, + "label": "@WM_TITAN2_OPTION_WASH_NORMAL_W" + }, + "SOILWASH_INTENSIVE": { + "index": 4, + "label": "@WM_TITAN2_OPTION_WASH_INTENSIVE_W" + } + } + }, + "spin": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_SPIN_SPEED_W", + "valueMapping": { + "NOT_SELECTED": { + "index": 0, + "label": "@WM_TERM_NO_SELECT_W" + }, + "NO_SPIN": { + "index": 1, + "label": "@WM_TITAN2_OPTION_SPIN_NO_SPIN_W" + }, + "SPIN_400": { + "index": 2, + "label": "@WM_TITAN2_OPTION_SPIN_400_W" + }, + "SPIN_600": { + "index": 3, + "label": "@WM_TITAN2_OPTION_SPIN_600_W" + }, + "SPIN_700": { + "index": 4, + "label": "@WM_TITAN2_OPTION_SPIN_700_W" + }, + "SPIN_800": { + "index": 5, + "label": "@WM_TITAN2_OPTION_SPIN_800_W" + }, + "SPIN_900": { + "index": 6, + "label": "@WM_TITAN2_OPTION_SPIN_900_W" + }, + "SPIN_1000": { + "index": 7, + "label": "@WM_TITAN2_OPTION_SPIN_1000_W" + }, + "SPIN_1100": { + "index": 8, + "label": "@WM_TITAN2_OPTION_SPIN_1100_W" + }, + "SPIN_1200": { + "index": 9, + "label": "@WM_TITAN2_OPTION_SPIN_1200_W" + }, + "SPIN_1400": { + "index": 10, + "label": "@WM_TITAN2_OPTION_SPIN_1400_W" + }, + "SPIN_1600": { + "index": 11, + "label": "@WM_TITAN2_OPTION_SPIN_1600_W" + }, + "SPIN_Max": { + "index": 255, + "label": "@WM_TITAN2_OPTION_SPIN_MAX_W" + } + } + }, + "temp": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_TEMP_W", + "valueMapping": { + "NO_TEMP": { + "index": 0, + "label": "@WM_TERM_NO_SELECT_W" + }, + "TEMP_COLD": { + "index": 1, + "label": "@WM_TITAN2_OPTION_TEMP_COLD_W" + }, + "TEMP_20": { + "index": 2, + "label": "@WM_TITAN2_OPTION_TEMP_20_W" + }, + "TEMP_30": { + "index": 3, + "label": "@WM_TITAN2_OPTION_TEMP_30_W" + }, + "TEMP_40": { + "index": 4, + "label": "@WM_TITAN2_OPTION_TEMP_40_W" + }, + "TEMP_50": { + "index": 5, + "label": "@WM_TITAN2_OPTION_TEMP_50_W" + }, + "TEMP_60": { + "index": 6, + "label": "@WM_TITAN2_OPTION_TEMP_60_W" + }, + "TEMP_95": { + "index": 7, + "label": "@WM_TITAN2_OPTION_TEMP_95_W" + } + } + }, + "rinse": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_RINSE_W", + "valueMapping": { + "NO_RINSE": { + "index": 0, + "label": "@WM_TERM_NO_SELECT_W" + }, + "RINSE_NORMAL": { + "index": 1, + "label": "@WM_TITAN2_OPTION_RINSE_NORMAL_W" + }, + "RINSE_PLUS": { + "index": 2, + "label": "@WM_TITAN2_OPTION_RINSE_RINSE+_W" + }, + "RINSE_PLUSPLUS": { + "index": 3, + "label": "@WM_TITAN2_OPTION_RINSE_RINSE++_W" + }, + "RINSE_NORMAL_HOLD": { + "index": 4, + "label": "@WM_TITAN2_OPTION_RINSE_NORMALHOLD_W" + }, + "RINSE_PLUS_HOLD": { + "index": 5, + "label": "@WM_TITAN2_OPTION_RINSE_RINSE+HOLD_W" + } + } + }, + "turboWash": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_TURBO_WASH_W", + "valueMapping": { + "TURBOWASH_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "TURBOWASH_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "steam": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_STEAM_W", + "valueMapping": { + "STEAM_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "STEAM_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "preWash": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_PRE_WASH_W", + "valueMapping": { + "PREWASH_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "PREWASH_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "medicRinse": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_MEDIC_RINSE_W", + "valueMapping": { + "MEDICRINSE_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "MEDICRINSE_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "steamSoftener": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_STEAM_SOFTENER_W", + "valueMapping": { + "STEAMSOFTENER_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "STEAMSOFTENER_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "loadItemWasher": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_LOAD_ITEM_W", + "valueMapping": { + "LOADITEM_OFF": { + "index": 0, + "label": "0" + }, + "LOADITEM_1": { + "index": 1, + "label": "1" + }, + "LOADITEM_2": { + "index": 2, + "label": "2" + }, + "LOADITEM_3": { + "index": 3, + "label": "3" + } + } + }, + "standby": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_STANDBY_W", + "valueMapping": { + "STANDBY_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "STANDBY_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "creaseCare": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_CREASE_CARE_W", + "valueMapping": { + "CREASECARE_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "CREASECARE_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "proofing": { + "dataType": "enum", + "label": "@WM_TITAN2_OPTION_PROOFING_W", + "valueMapping": { + "PROOFING_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "PROOFING_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "courseFL24inchBaseTitan": { + "ref": "Course" + }, + "error": { + "dataType": "enum", + "valueMapping": { + "ERROR_NO": { + "_comment": "No Error", + "index": 0, + "label": "ERROR_NOERROR", + "title": "ERROR_NOERROR_TITLE", + "content": "ERROR_NOERROR_CONTENT" + }, + "ERROR_DE2": { + "_comment": "DE2 Error", + "index": 1, + "label": "@WM_WW_FL_ERROR_DE2_W", + "title": "@WM_WW_FL_ERROR_DE2_TITLE_W", + "content": "@WM_WW_FL_ERROR_DE2_CONTENT_S" + }, + "ERROR_DE1": { + "_comment": "DE1 Error", + "index": 2, + "label": "@WM_WW_FL_ERROR_DE1_W", + "title": "@WM_WW_FL_ERROR_DE1_TITLE_W", + "content": "@WM_WW_FL_ERROR_DE1_CONTENT_S" + }, + "ERROR_IE": { + "_comment": "IE Error", + "index": 3, + "label": "@WM_WW_FL_ERROR_IE_W", + "title": "@WM_WW_FL_ERROR_IE_TITLE_W", + "content": "@WM_WW_FL_ERROR_IE_CONTENT_S" + }, + "ERROR_OE": { + "_comment": "OE Error", + "index": 4, + "label": "@WM_WW_FL_ERROR_OE_W", + "title": "@WM_WW_FL_ERROR_OE_TITLE_W", + "content": "@WM_WW_FL_ERROR_OE_CONTENT_S" + }, + "ERROR_UE": { + "_comment": "UE Error", + "index": 5, + "label": "@WM_WW_FL_ERROR_UE_W", + "title": "@WM_WW_FL_ERROR_UE_TITLE_W", + "content": "@WM_WW_FL_ERROR_UE_CONTENT_S" + }, + "ERROR_FE": { + "_comment": "FE Error", + "index": 6, + "label": "@WM_WW_FL_ERROR_FE_W", + "title": "@WM_WW_FL_ERROR_FE_TITLE_W", + "content": "@WM_WW_FL_ERROR_FE_CONTENT_S" + }, + "ERROR_PE": { + "_comment": "PE Error", + "index": 7, + "label": "@WM_WW_FL_ERROR_PE_W", + "title": "@WM_WW_FL_ERROR_PE_TITLE_W", + "content": "@WM_WW_FL_ERROR_PE_CONTENT_S" + }, + "ERROR_TE": { + "_comment": "tE error", + "index": 8, + "label": "@WM_WW_FL_ERROR_TE_W", + "title": "@WM_WW_FL_ERROR_TE_TITLE_W", + "content": "@WM_WW_FL_ERROR_TE_CONTENT_S" + }, + "ERROR_LE": { + "_comment": "LE error", + "index": 9, + "label": "@WM_WW_FL_ERROR_LE_W", + "title": "@WM_WW_FL_ERROR_LE_TITLE_W", + "content": "@WM_WW_FL_ERROR_LE_CONTENT_S" + }, + "ERROR_DHE": { + "_comment": "dHE error", + "index": 11, + "label": "@WM_WW_FL_ERROR_DHE_W", + "title": "@WM_WW_FL_ERROR_DHE_TITLE_W", + "content": "@WM_WW_FL_ERROR_DHE_CONTENT_S" + }, + "ERROR_PF": { + "_comment": "PF error", + "index": 12, + "label": "@WM_WW_FL_ERROR_PF_W", + "title": "@WM_WW_FL_ERROR_PF_TITLE_W", + "content": "@WM_WW_FL_ERROR_PF_CONTENT_S" + }, + "ERROR_FF": { + "_comment": "FF error", + "index": 13, + "label": "@WM_WW_FL_ERROR_FF_W", + "title": "@WM_WW_FL_ERROR_FF_TITLE_W", + "content": "@WM_WW_FL_ERROR_FF_CONTENT_S" + }, + "ERROR_DCE": { + "_comment": "dCE Error", + "index": 14, + "label": "@WM_WW_FL_ERROR_DCE_W", + "title": "@WM_WW_FL_ERROR_DCE_TITLE_W", + "content": "@WM_WW_FL_ERROR_DCE_CONTENT_S" + }, + "ERROR_AE": { + "_comment": "AE Error (AquaLock)", + "index": 15, + "label": "@WM_WW_FL_ERROR_AE_W", + "title": "@WM_WW_FL_ERROR_AE_TITLE_W", + "content": "@WM_WW_FL_ERROR_AE_CONTENT_S" + }, + "ERROR_EE": { + "_comment": "EE error", + "index": 16, + "label": "@WM_WW_FL_ERROR_EE_W", + "title": "@WM_WW_FL_ERROR_EE_TITLE_W", + "content": "@WM_WW_FL_ERROR_EE_CONTENT_S" + }, + "ERROR_PS": { + "_comment": "PS Error", + "index": 17, + "label": "@WM_WW_FL_ERROR_PS_W", + "title": "@WM_WW_FL_ERROR_PS_TITLE_W", + "content": "@WM_WW_FL_ERROR_PS_CONTENT_S" + }, + "ERROR_DE4": { + "_comment": "dE4 Error", + "index": 18, + "label": "@WM_WW_FL_ERROR_DE4_W", + "title": "@WM_WW_FL_ERROR_DE4_TITLE_W", + "content": "@WM_WW_FL_ERROR_DE4_CONTENT_S" + }, + "ERROR_VS": { + "_comment": "vS Error", + "index": 19, + "label": "@WM_WW_FL_ERROR_VS_W", + "title": "@WM_WW_FL_ERROR_VS_TITLE_W", + "content": "@WM_WW_FL_ERROR_VS_CONTENT_S" + } + } + }, + "smartCourseFL24inchBaseTitan": { + "ref": "SmartCourse" + }, + "doorLock": { + "dataType": "enum", + "label": null, + "valueMapping": { + "DOOR_LOCK_OFF": { + "index": 0, + "label": "@CP_OFF_EN_W" + }, + "DOOR_LOCK_ON": { + "index": 1, + "label": "@CP_ON_EN_W" + } + } + }, + "downloadedCourseFL24inchBaseTitan": { + "ref": "SmartCourse" + } + }, + "ControlWifi": { + "WMStart": { + "command": "Set", + "data": { + "washerDryer": { + "course": "temp", + "soilWash": "NO_SOILWASH", + "spin": "NOT_SELECTED", + "temp": "NO_TEMP", + "rinse": "NO_RINSE", + "reserveTimeHour": 0, + "reserveTimeMinute": 0, + "loadItemWasher": "LOADITEM_OFF", + "turboWash": "TURBOWASH_OFF", + "creaseCare": "CREASECARE_OFF", + "steamSoftener": "STEAMSOFTENER_OFF", + "ecoHybrid": "ECOHYBRID_OFF", + "medicRinse": "MEDICRINSE_OFF", + "rinseSpin": "RINSE_SPIN_OFF", + "preWash": "PREWASH_OFF", + "steam": "STEAM_OFF", + "initialBit": "INITIAL_BIT_OFF", + "remoteStart": "REMOTE_START_OFF", + "doorLock": "DOOR_LOCK_OFF", + "childLock": "CHILDLOCK_OFF", + "SmartCourse": "temp" + } + } + }, + "WMDownload": { + "command": "Set", + "data": { + "washerDryer": { + "courseDownloadType": "COURSEDATA", + "courseDownloadDataLength": 21, + "course": "temp", + "soilWash": "NO_SOILWASH", + "spin": "NOT_SELECTED", + "temp": "NO_TEMP", + "rinse": "NO_RINSE", + "reserveTimeHour": 0, + "reserveTimeMinute": 0, + "loadItemWasher": "LOADITEM_OFF", + "turboWash": "TURBOWASH_OFF", + "creaseCare": "CREASECARE_OFF", + "steamSoftener": "STEAMSOFTENER_OFF", + "ecoHybrid": "ECOHYBRID_OFF", + "medicRinse": "MEDICRINSE_OFF", + "rinseSpin": "RINSE_SPIN_OFF", + "preWash": "PREWASH_OFF", + "steam": "STEAM_OFF", + "initialBit": "INITIAL_BIT_OFF", + "remoteStart": "REMOTE_START_OFF", + "doorLock": "DOOR_LOCK_OFF", + "childLock": "CHILDLOCK_OFF", + "SmartCourse": "temp" + } + } + }, + "WMOff": { + "command": "Set", + "data": { + "washerDryer": { + "controlDataType": "POWEROFF", + "controlDataValueLength": 1, + "controlDataValue": 0 + } + } + }, + "WMStop": { + "command": "Set", + "data": { + "washerDryer": { + "controlDataType": "PAUSE", + "controlDataValueLength": 1, + "controlDataValue": 0 + } + } + }, + "WMWakeup": { + "command": "Set", + "data": { + "washerDryer": { + "controlDataType": "WAKEUP", + "controlDataValueLength": 0 + } + } + } + }, + "Course": { + "COTTON": { + "_comment": "Cotton", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_COTTON_W", + "script": "", + "controlEnable": true, + "courseValue": "COTTON", + "imgIndex": 141, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_40", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40", + "TEMP_60", + "TEMP_95" + ] + }, + { + "value": "spin", + "default": "SPIN_Max", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000", + "SPIN_Max" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "EASYCARE": { + "_comment": "Easy Care", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_EASY_CARE_W", + "script": "", + "controlEnable": true, + "courseValue": "EASYCARE", + "imgIndex": 145, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_40", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40", + "TEMP_60" + ] + }, + { + "value": "spin", + "default": "SPIN_Max", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000", + "SPIN_Max" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "COTTONPLUS": { + "_comment": "Eco 40-60", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_ECO_40_60_W", + "script": "", + "controlEnable": true, + "courseValue": "COTTONPLUS", + "imgIndex": 148, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_40", + "selectable": [ + "TEMP_40", + "TEMP_60" + ] + }, + { + "value": "spin", + "default": "SPIN_Max", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000", + "SPIN_Max" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "DUVET": { + "_comment": "Duvet", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_DUVET_W", + "script": "", + "controlEnable": true, + "courseValue": "DUVET", + "imgIndex": 202, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_COLD", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40" + ] + }, + { + "value": "spin", + "default": "SPIN_1000", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "MIXEDFABRIC": { + "_comment": "Mix", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_MIX_W", + "script": "", + "controlEnable": true, + "courseValue": "MIXEDFABRIC", + "imgIndex": 142, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_40", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40", + "TEMP_60" + ] + }, + { + "value": "spin", + "default": "SPIN_1000", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000", + "SPIN_Max" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SPORTSWEAR": { + "_comment": "Sports Wear", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_SPORTS_WEAR_W", + "script": "", + "controlEnable": true, + "courseValue": "SPORTSWEAR", + "imgIndex": 51, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_40", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40" + ] + }, + { + "value": "spin", + "default": "SPIN_800", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SILENTWASH": { + "_comment": "Silent Wash", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_SILENT_WASH_W", + "script": "", + "controlEnable": true, + "courseValue": "SILENTWASH", + "imgIndex": 26, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_40", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40", + "TEMP_60" + ] + }, + { + "value": "spin", + "default": "SPIN_800", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SPEED14": { + "_comment": "Speed 14", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_SPEED_14_W", + "script": "", + "controlEnable": true, + "courseValue": "SPEED14", + "imgIndex": 143, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_20", + "selectable": [ + "TEMP_20", + "TEMP_30", + "TEMP_40" + ] + }, + { + "value": "spin", + "default": "SPIN_400", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000", + "SPIN_Max" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_ON" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "TUB_CLEAN": { + "_comment": "Tub Clean", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_TUB_CLEAN_W", + "script": "", + "controlEnable": true, + "courseValue": "TUB_CLEAN", + "imgIndex": 152, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL", + "showing": "@WM_TERM_NO_SELECT_W" + }, + { + "value": "temp", + "default": "TEMP_60", + "showing": "@WM_TERM_NO_SELECT_W" + }, + { + "value": "spin", + "default": "NO_SPIN", + "showing": "@WM_TERM_NO_SELECT_W" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "showing": "@WM_TERM_NO_SELECT_W" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + } + ] + }, + "WOOL": { + "_comment": "Wool", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_WOOL_W", + "script": "", + "controlEnable": true, + "courseValue": "WOOL", + "imgIndex": 99, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_30", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40" + ] + }, + { + "value": "spin", + "default": "SPIN_800", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "DELICATE": { + "_comment": "Delicate", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_DELICATE_W", + "script": "", + "controlEnable": true, + "courseValue": "DELICATE", + "imgIndex": 149, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_20", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40" + ] + }, + { + "value": "spin", + "default": "SPIN_800", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "ALLERGYSPASTEAM": { + "_comment": "Allergy SpaSteam", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_ALLERGY_SPASTEAM_W", + "script": "", + "controlEnable": true, + "courseValue": "ALLERGYSPASTEAM", + "imgIndex": 147, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_60" + }, + { + "value": "spin", + "default": "SPIN_Max", + "selectable": [ + "NO_SPIN", + "SPIN_400", + "SPIN_800", + "SPIN_1000", + "SPIN_Max" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_ON" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "TURBO39": { + "_comment": "Turbo Wash 39", + "courseType": "Course", + "name": "@WM_WW_FL_TITAN2_COURSE_TURBO_39_W", + "script": "", + "controlEnable": true, + "courseValue": "TURBO39", + "imgIndex": 212, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "temp", + "default": "TEMP_40", + "selectable": [ + "TEMP_COLD", + "TEMP_20", + "TEMP_30", + "TEMP_40", + "TEMP_60" + ] + }, + { + "value": "spin", + "default": "SPIN_1200", + "selectable": [ + "SPIN_400", + "SPIN_800", + "SPIN_1000", + "SPIN_1200", + "SPIN_Max" + ] + }, + { + "value": "rinse", + "default": "RINSE_NORMAL", + "selectable": [ + "RINSE_NORMAL", + "RINSE_PLUS" + ] + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_ON" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + } + }, + "SmartCourse": { + "BABYCARE": { + "_comment": "baby_care", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "BABYCARE", + "name": "@WM_WW_FL_SMARTCOURSE_BABY_CARE_W", + "script": "@WM_WW_FL_SMARTCOURSE_BABY_CARE_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 52, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_1000" + }, + { + "value": "temp", + "default": "TEMP_60" + }, + { + "value": "rinse", + "default": "RINSE_PLUS" + }, + { + "value": "preWash", + "default": "PREWASH_ON" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_ON", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "HYGIENE": { + "_comment": "Hygiene", + "Course": "ALLERGYCARE", + "courseType": "SmartCourse", + "courseValue": "HYGIENE", + "name": "@WM_WW_FL_SMARTCOURSE_HYGIENE_W", + "script": "@WM_WW_FL_SMARTCOURSE_HYGIENE_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 36, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_60" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_ON" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SMALLLOAD": { + "_comment": "Small Load", + "Course": "SPEED14", + "courseType": "SmartCourse", + "courseValue": "SMALLLOAD", + "name": "@WM_WW_FL_SMARTCOURSE_SMALL_LOAD_W", + "script": "@WM_WW_FL_SMARTCOURSE_SMALL_LOAD_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 46, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_400" + }, + { + "value": "temp", + "default": "TEMP_20" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_ON" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "LINGERIE": { + "_comment": "Lingerie", + "Course": "DELICATE", + "courseType": "SmartCourse", + "courseValue": "LINGERIE", + "name": "@WM_WW_FL_SMARTCOURSE_LINGERIE_W", + "script": "@WM_WW_FL_SMARTCOURSE_LINGERIE_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 13, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_800" + }, + { + "value": "temp", + "default": "TEMP_20" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SKINCARE": { + "_comment": "skin_care", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "SKINCARE", + "name": "@WM_WW_FL_SMARTCOURSE_SKIN_CARE_W", + "script": "@WM_WW_FL_SMARTCOURSE_SKIN_CARE_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 16, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_PLUS" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_ON", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "COLDWASH": { + "_comment": "Cold Wash", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "COLDWASH", + "name": "@WM_WW_FL_SMARTCOURSE_COLD_WASH_W", + "script": "@WM_WW_FL_SMARTCOURSE_COLD_WASH_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 22, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_COLD" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "RINSESPIN": { + "_comment": "Rinse + Spin", + "Course": "RINSESPIN", + "courseType": "SmartCourse", + "courseValue": "RINSESPIN", + "name": "@WM_WW_FL_SMARTCOURSE_RINSE_SPIN_W", + "script": "@WM_WW_FL_SMARTCOURSE_RINSE_SPIN_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 60, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "NO_TEMP" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "KIDSWEAR": { + "_comment": "Kids Wear", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "KIDSWEAR", + "name": "@WM_WW_FL_SMARTCOURSE_KIDS_WEAR_W", + "script": "@WM_WW_FL_SMARTCOURSE_KIDS_WEAR_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 53, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_1200" + }, + { + "value": "temp", + "default": "TEMP_60" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_ON" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SCHOOLUNIFORM": { + "_comment": "School Uniform", + "Course": "EASYCARE", + "courseType": "SmartCourse", + "courseValue": "SCHOOLUNIFORM", + "name": "@WM_WW_FL_SMARTCOURSE_SCHOOL_UNIFORM_W", + "script": "@WM_WW_FL_SMARTCOURSE_SCHOOL_UNIFORM_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 130, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_1000" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SWIMMINGWEAR": { + "_comment": "Swimming Wear", + "Course": "WOOL", + "courseType": "SmartCourse", + "courseValue": "SWIMMINGWEAR", + "name": "@WM_WW_FL_SMARTCOURSE_SWIMMING_WEAR_W", + "script": "@WM_WW_FL_SMARTCOURSE_SWIMMING_WEAR_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 54, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_400" + }, + { + "value": "temp", + "default": "TEMP_20" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "RAINYSEASON": { + "_comment": "Rainy Season", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "RAINYSEASON", + "name": "@WM_WW_FL_SMARTCOURSE_RAINY_SEASON_W", + "script": "@WM_WW_FL_SMARTCOURSE_RAINY_SEASON_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 55, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "GYMCLOTHES": { + "_comment": "Gym Clothes", + "Course": "SPORTSWEAR", + "courseType": "SmartCourse", + "courseValue": "GYMCLOTHES", + "name": "@WM_WW_FL_SMARTCOURSE_GYM_CLOTHES_W", + "script": "@WM_WW_FL_SMARTCOURSE_GYM_CLOTHES_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 56, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_800" + }, + { + "value": "temp", + "default": "TEMP_COLD" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "JEANS": { + "_comment": "Jeans", + "Course": "DELICATE", + "courseType": "SmartCourse", + "courseValue": "JEANS", + "name": "@WM_WW_FL_SMARTCOURSE_JEANS_W", + "script": "@WM_WW_FL_SMARTCOURSE_JEANS_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 76, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_20" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "BLANKET": { + "_comment": "Blanket", + "Course": "DUVET", + "courseType": "SmartCourse", + "courseValue": "BLANKET", + "name": "@WM_WW_FL_SMARTCOURSE_BLANKET_W", + "script": "@WM_WW_FL_SMARTCOURSE_BLANKET_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 57, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_400" + }, + { + "value": "temp", + "default": "TEMP_COLD" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SWEATSTAIN": { + "_comment": "Sweat Stain", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "SWEATSTAIN", + "name": "@WM_WW_FL_SMARTCOURSE_SWEAT_STAIN_W", + "script": "@WM_WW_FL_SMARTCOURSE_SWEAT_STAIN_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 58, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SINGLEGARMENT": { + "_comment": "Single Garment", + "Course": "SPEED14", + "courseType": "SmartCourse", + "courseValue": "SINGLEGARMENT", + "name": "@WM_WW_FL_SMARTCOURSE_SINGLE_GARMENT_W", + "script": "@WM_WW_FL_SMARTCOURSE_SINGLE_GARMENT_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 59, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_400" + }, + { + "value": "temp", + "default": "TEMP_20" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_ON" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "COLORPROTECTION": { + "_comment": "Color Protection", + "Course": "DELICATE", + "courseType": "SmartCourse", + "courseValue": "COLORPROTECTION", + "name": "@WM_WW_FL_SMARTCOURSE_COLOR_PROTECTION_W", + "script": "@WM_WW_FL_SMARTCOURSE_COLOR_PROTECTION_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 47, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_800" + }, + { + "value": "temp", + "default": "TEMP_20" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "NOISEMINIMIZE": { + "_comment": "Noise Minimize", + "Course": "SILENTWASH", + "courseType": "SmartCourse", + "courseValue": "NOISEMINIMIZE", + "name": "@WM_WW_FL_SMARTCOURSE_NOISE_MINIMIZE_W", + "script": "@WM_WW_FL_SMARTCOURSE_NOISE_MINIMIZE_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 88, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_1000" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "MINIMIZEWRINKLES": { + "_comment": "Minimize Wrinkles", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "MINIMIZEWRINKLES", + "name": "@WM_WW_FL_SMARTCOURSE_MINIMIZE_WRINKLES_W", + "script": "@WM_WW_FL_SMARTCOURSE_MINIMIZE_WRINKLES_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 80, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "LIGHTLYSOILEDITEMS": { + "_comment": "Lightly Soiled Items", + "Course": "WOOL", + "courseType": "SmartCourse", + "courseValue": "LIGHTLYSOILEDITEMS", + "name": "@WM_WW_FL_SMARTCOURSE_LIGHTLY_SOILED_ITEMS_W", + "script": "@WM_WW_FL_SMARTCOURSE_LIGHTLY_SOILED_ITEMS_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 43, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_400" + }, + { + "value": "temp", + "default": "TEMP_20" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "MINIMIZEDETERGENT": { + "_comment": "Minimize Detergent Residue", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "MINIMIZEDETERGENT", + "name": "@WM_WW_FL_SMARTCOURSE_MINIMIZE_DETERGENT_RESIDUE_W", + "script": "@WM_WW_FL_SMARTCOURSE_MINIMIZE_DETERGENT_RESIDUE_1_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 30, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_PLUS" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SLEEVEHEMSANDCOLLARS": { + "_comment": "Sleeve Hems and Collars", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "SLEEVEHEMSANDCOLLARS", + "name": "@WM_WW_FL_SMARTCOURSE_SLEEVE_HEMS_AND_COLLARS_W", + "script": "@WM_WW_FL_SMARTCOURSE_SLEEVE_HEMS_AND_COLLARS_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 37, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_1000" + }, + { + "value": "temp", + "default": "TEMP_60" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_ON" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "JUICEANDFOODSTAINS": { + "_comment": "Juice and Food Stains", + "Course": "COTTON", + "courseType": "SmartCourse", + "courseValue": "JUICEANDFOODSTAINS", + "name": "@WM_WW_FL_SMARTCOURSE_JUICE_AND_FOOD_STAINS_W", + "script": "@WM_WW_FL_SMARTCOURSE_JUICE_AND_FOOD_STAINS_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 108, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_1000" + }, + { + "value": "temp", + "default": "TEMP_40" + }, + { + "value": "rinse", + "default": "RINSE_NORMAL" + }, + { + "value": "preWash", + "default": "PREWASH_ON" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "QUICKTUBCLEAN": { + "_comment": "quick_tub_clean", + "Course": "QUICKTUBCLEAN", + "courseType": "SmartCourse", + "courseValue": "QUICKTUBCLEAN", + "name": "@WM_WW_FL_SMARTCOURSE_QUICK_TUB_CLEAN_W", + "script": "@WM_WW_FL_SMARTCOURSE_QUICK_TUB_CLEAN_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 38, + "function": [ + { + "value": "soilWash", + "default": "SOILWASH_NORMAL" + }, + { + "value": "spin", + "default": "SPIN_400" + }, + { + "value": "temp", + "default": "TEMP_COLD" + }, + { + "value": "rinse", + "default": "NO_RINSE" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + } + ] + }, + "DRAIN": { + "_comment": "Drain", + "Course": "SPINONLY", + "courseType": "SmartCourse", + "courseValue": "DRAIN", + "name": "@WM_WW_FL_SMARTCOURSE_DRAIN_W", + "script": "@WM_WW_FL_SMARTCOURSE_DRAIN_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 217, + "function": [ + { + "value": "soilWash", + "default": "NO_SOILWASH" + }, + { + "value": "spin", + "default": "NO_SPIN" + }, + { + "value": "temp", + "default": "NO_TEMP" + }, + { + "value": "rinse", + "default": "NO_RINSE" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + }, + "SPIN": { + "_comment": "Spin", + "Course": "SPINONLY", + "courseType": "SmartCourse", + "courseValue": "SPIN", + "name": "@WM_WW_FL_SMARTCOURSE_SPIN_W", + "script": "@WM_WW_FL_SMARTCOURSE_SPIN_SCRIPT_S", + "downloadEnable": true, + "controlEnable": true, + "imgIndex": 27, + "function": [ + { + "value": "soilWash", + "default": "NO_SOILWASH" + }, + { + "value": "spin", + "default": "SPIN_Max" + }, + { + "value": "temp", + "default": "NO_TEMP" + }, + { + "value": "rinse", + "default": "NO_RINSE" + }, + { + "value": "preWash", + "default": "PREWASH_OFF" + }, + { + "value": "turboWash", + "default": "TURBOWASH_OFF" + }, + { + "value": "steam", + "default": "STEAM_OFF" + }, + { + "value": "medicRinse", + "default": "MEDICRINSE_OFF", + "visibility": "gone" + }, + { + "value": "loadItemWasher", + "default": "LOADITEM_OFF", + "visibility": "gone" + }, + { + "value": "reserveTimeHour", + "default": 0 + } + ] + } + }, + "Push": [ + { + "category": "PUSH_WM_STATE", + "label": "@CP_ALARM_PRODUCT_STATE_W", + "groupCode": "20101", + "pushList": [ + { + "0000": "PUSH_WM_COMPLETE" + }, + { + "0001": "PUSH_WM_REMOTE_ANOTHER_ID" + }, + { + "0100": "PUSH_WM_ERROR" + }, + { + "0200": "PUSH_WM_REMOTE_START_OFF" + }, + { + "0201": "PUSH_WM_REMOTE_START_ON" + } + ] + } + ], + "EnergyMonitoring": { + "valueMapping": [ + "temp", + "spin", + "soilWash" + ], + "powertable": { + "1": 1, + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6 + }, + "watertable": null + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/user-info-response-1.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/user-info-response-1.json new file mode 100644 index 0000000000000..f0284803c9ba8 --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/user-info-response-1.json @@ -0,0 +1,52 @@ +{ + "status":1, + "account":{ + "userID":"%s", + "userNo":"BR2005200239023", + "userIDType":"LGE", + "displayUserID":"faker", + "userIDList":[ + { + "lgeIDList":[ + { + "lgeIDType":"LGE", + "userID":"%s" + } + ] + } + ], + "dateOfBirth":"05-05-1978", + "country":"BR", + "countryName":"Brazil", + "blacklist":"N", + "age":"45", + "isSubscribe":"N", + "changePw":"N", + "toEmailId":"N", + "periodPW":"N", + "lgAccount":"Y", + "isService":"Y", + "userNickName":"faker", + "authUser":"N", + "serviceList":[ + { + "isService":"Y", + "svcName":"LG ThinQ", + "svcCode":"SVC202", + "joinDate":"29-05-2018" + }, + { + "isService":"Y", + "svcName":"LG Developer", + "svcCode":"SVC609", + "joinDate":"29-05-2018" + }, + { + "isService":"Y", + "svcName":"MC OAuth", + "svcCode":"SVC710", + "joinDate":"29-05-2018" + } + ] + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.lgthinq/src/test/resources/wm-data-result.json b/bundles/org.openhab.binding.lgthinq/src/test/resources/wm-data-result.json new file mode 100644 index 0000000000000..17cb8f393da3c --- /dev/null +++ b/bundles/org.openhab.binding.lgthinq/src/test/resources/wm-data-result.json @@ -0,0 +1,118 @@ +{ + "resultCode": "0000", + "result": { + "appType":"NUTS", + "modelCountryCode":"WW", + "countryCode":"DK", + "modelName":"F_R7_Y___W.A__QEUK", + "deviceType":201, + "deviceCode":"LA02", + "alias":"Frontbetjent vaskemaskine", + "deviceId":"592bd2a4-d3e7-16e9-a69f-44cb8b2e0c43", + "fwVer":"", + "imageFileName":"home_appliances_img_wmdrum.png", + "ssid":"Kepler", + "softapId":"", + "softapPass":"", + "macAddress":"", + "networkType":"02", + "timezoneCode":"Europe/Copenhagen", + "timezoneCodeAlias":"Europe/Copenhagen", + "utcOffset":1, + "utcOffsetDisplay":"+01:00", + "dstOffset":2, + "dstOffsetDisplay":"+02:00", + "curOffset":1, + "curOffsetDisplay":"+01:00", + "sdsGuide":"{\"deviceCode\":\"LA02\"}", + "newRegYn":"N", + "remoteControlType":"", + "userNo":"DK2202075642801", + "tftYn":"N", + "deviceState":"E", + "snapshot":{ + "washerDryer":{ + "initialBit":"INITIAL_BIT_OFF", + "standby":"STANDBY_OFF", + "courseFL24inchBaseTitan":"MIXEDFABRIC", + "initialTimeMinute":24.0, + "preState":"SPINNING", + "error":"ERROR_NO", + "dryLevel":"NOT_SELECTED", + "creaseCare":"CREASECARE_OFF", + "remainTimeHour":0.0, + "smartCourseFL24inchBaseTitan":"NOT_SELECTED", + "preWash":"PREWASH_OFF", + "steam":"STEAM_OFF", + "state":"SPINNING", + "rinse":"NO_RINSE", + "wrinkleCare":"WRINKLECARE_OFF", + "loadItemWasher":"LOADITEM_OFF", + "temp":"NO_TEMP", + "doorLock":"DOOR_LOCK_ON", + "reserveTimeMinute":0.0, + "AIDDLed":"AIDDLed_OFF", + "TCLCount":33.0, + "downloadedCourseFL24inchBaseTitan":"RINSESPIN", + "medicRinse":"MEDICRINSE_OFF", + "turboWash":"TURBOWASH_OFF", + "ecoHybrid":"ECOHYBRID_OFF", + "remainTimeMinute":11.0, + "reserveTimeHour":0.0, + "steamSoftener":"STEAMSOFTENER_OFF", + "childLock":"CHILDLOCK_OFF", + "remoteStart":"REMOTE_START_ON", + "spin":"SPIN_1400", + "soilWash":"NO_SOILWASH", + "rinseSpin":"RINSE_SPIN_OFF", + "initialTimeHour":1.0 + }, + "mid":8.4022883E7, + "online":true, + "static":{ + "deviceType":"201", + "countryCode":"DK" + }, + "meta":{ + "allDeviceInfoUpdate":false, + "messageId":"TSmRTV6yTUq2obot8_Q9Qg" + }, + "timestamp":1.644358361572E12 + }, + "online":true, + "platformType":"thinq2", + "area":125955, + "regDt":2.0220208013031E13, + "blackboxYn":"Y", + "modelProtocol":"courseFL24inchBaseTitan", + "receipeVersion":0, + "activeSaving":"OFF", + "smartCareV2":"OFF", + "order":0, + "nlpAlias":"none", + "drServiceYn":"N", + "fwInfoList":[ + { + "checksum":"016771B3", + "partNumber":"SAA41059310", + "order":2.0 + }, + { + "checksum":"000075A3", + "partNumber":"SAA41059211", + "order":1.0 + } + ], + "regDtUtc":"20220207233031", + "regIndex":0, + "groupableYn":"N", + "controllableYn":"N", + "combinedProductYn":"N", + "masterYn":"Y", + "controlGuideType":"TYPE4", + "initDevice":false, + "upgradableYn":"N", + "autoFwDownloadYn":"N", + "tclcount":0 + } +} \ No newline at end of file diff --git a/bundles/pom.xml b/bundles/pom.xml index 2e9c12d782a34..fced255d5bc46 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -223,6 +223,7 @@ org.openhab.binding.leapmotion org.openhab.binding.lghombot org.openhab.binding.lgtvserial + org.openhab.binding.lgthinq org.openhab.binding.lgwebos org.openhab.binding.lifx org.openhab.binding.linky