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 extends CapabilityDefinition, ? extends SnapshotDefinition> thing);
+
+ /**
+ * Unregistry the thing
+ *
+ * @param thing to be unregistered
+ */
+ void unRegistryListenerThing(
+ LGThinQAbstractDeviceHandler extends CapabilityDefinition, ? extends SnapshotDefinition> 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 extends CapabilityDefinition, ? extends SnapshotDefinition> 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 extends CapabilityDefinition, ? extends SnapshotDefinition> 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 extends CapabilityDefinition, ? extends SnapshotDefinition> 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