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 00000000000..f2dfa868fb0
--- /dev/null
+++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiClientService.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2025 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} interface provides a common abstraction for handling AC-related
+ * API interactions with LG ThinQ devices. It supports both protocol versions V1 and V2.
+ *
+ * This interface allows external components to change various air conditioner settings, such as
+ * operation mode, fan speed, temperature, and additional features like Jet Mode and Energy Saving Mode.
+ *
+ *
+ * @author Nemer Daud - Initial contribution
+ */
+@NonNullByDefault
+public interface LGThinQACApiClientService extends LGThinQApiClientService {
+
+ /**
+ * Changes the air conditioner's operation mode (e.g., Cool, Heat, Fan).
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param newOpMode The new operation mode to be set.
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void changeOperationMode(String bridgeName, String deviceId, int newOpMode) throws LGThinqApiException;
+
+ /**
+ * Adjusts the fan speed of the air conditioner.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param newFanSpeed The desired fan speed level.
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void changeFanSpeed(String bridgeName, String deviceId, int newFanSpeed) throws LGThinqApiException;
+
+ /**
+ * Adjusts the vertical orientation of the AC fan.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param currentSnap The current snapshot of AC device data.
+ * @param newStep The new vertical position.
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void changeStepUpDown(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep)
+ throws LGThinqApiException;
+
+ /**
+ * Adjusts the horizontal orientation of the AC fan.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param currentSnap The current snapshot of AC device data.
+ * @param newStep The new horizontal position.
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void changeStepLeftRight(String bridgeName, String deviceId, ACCanonicalSnapshot currentSnap, int newStep)
+ throws LGThinqApiException;
+
+ /**
+ * Changes the target temperature of the air conditioner.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param newTargetTemp The new target temperature to be set.
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void changeTargetTemperature(String bridgeName, String deviceId, ACTargetTmp newTargetTemp)
+ throws LGThinqApiException;
+
+ /**
+ * Enables or disables the Jet Mode feature.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param modeOnOff The desired state ("on" to enable, "off" to disable).
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void turnCoolJetMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException;
+
+ /**
+ * Enables or disables the Air Clean mode.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param modeOnOff The desired state ("on" to enable, "off" to disable).
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void turnAirCleanMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException;
+
+ /**
+ * Enables or disables the Auto Dry feature.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param modeOnOff The desired state ("on" to enable, "off" to disable).
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void turnAutoDryMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException;
+
+ /**
+ * Enables or disables the Energy Saving mode.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @param modeOnOff The desired state ("on" to enable, "off" to disable).
+ * @throws LGThinqApiException If an error occurs while invoking the LG API.
+ */
+ void turnEnergySavingMode(String bridgeName, String deviceId, String modeOnOff) throws LGThinqApiException;
+
+ /**
+ * Retrieves extended device information, such as energy consumption and filter status.
+ *
+ * @param bridgeName The name of the bridge managing the device connection.
+ * @param deviceId The unique ID of the LG ThinQ AC device.
+ * @return An {@link ExtendedDeviceInfo} object containing the extended data of the device.
+ * @throws LGThinqApiException If an error occurs while invoking the 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 00000000000..59929c4611f
--- /dev/null
+++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQACApiV1ClientServiceImpl.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2010-2025 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.*;
+
+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 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 00000000000..d0fadbb0dec
--- /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-2025 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 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 00000000000..dab0cecd97b
--- /dev/null
+++ b/bundles/org.openhab.binding.lgthinq/src/main/java/org/openhab/binding/lgthinq/lgservices/LGThinQAbstractApiClientService.java
@@ -0,0 +1,525 @@
+/*
+ * Copyright (c) 2010-2025 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.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
+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 {
+ protected final ObjectMapper objectMapper = new ObjectMapper();
+ protected final TokenManager tokenManager;
+ protected final Class capabilityClass;
+ protected final Class snapshotClass;
+ protected final HttpClient httpClient;
+ private final Logger logger = LoggerFactory.getLogger(LGThinQAbstractApiClientService.class);
+ private final String clientId = "";
+
+ protected LGThinQAbstractApiClientService(Class capabilityClass, Class snapshotClass, HttpClient httpClient) {
+ this.httpClient = httpClient;
+ this.tokenManager = new TokenManager(httpClient);
+ this.capabilityClass = capabilityClass;
+ this.snapshotClass = snapshotClass;
+ }
+
+ protected static String getErrorCodeMessage(@Nullable String code) {
+ if (code == null) {
+ return "";
+ }
+ ResultCodes resultCode = ResultCodes.fromCode(code);
+ return resultCode.getDescription();
+ }
+
+ /**
+ * Retrieves the client ID based on the provided user number.
+ *
+ * @param userNumber the user number to generate the client ID
+ * @return the generated client ID
+ */
+ private String getClientId(String userNumber) {
+ if (!clientId.isEmpty()) {
+ return clientId;
+ }
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ String data = userNumber + Instant.now().toString();
+ byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
+ return bytesToHex(hash);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("SHA-256 algorithm not found", e);
+ }
+ }
+
+ private String bytesToHex(byte[] bytes) {
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : bytes) {
+ hexString.append(String.format("%02x", b));
+ }
+ return hexString.toString();
+ }
+
+ 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-app-version", "LG ThinQ/5.0.28271");
+ headers.put("x-client-id", getClientId(userNumber));
+ 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-app-logintype", "LGE");
+ headers.put("x-origin", "app-native");
+ headers.put("x-device-type", "601");
+
+ 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 information 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(getBaseCapConfigDataFile(), deviceId));
+ try {
+ if (!regFile.isFile() || forceRecreate) {
+ try (InputStream in = new URI(uri).toURL().openStream()) {
+ Files.copy(in, regFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ } catch (IOException | URISyntaxException 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);
+ }
+
+ /**
+ * Handles the result of device settings retrieved from an API call.
+ *
+ * @param resp The RestResult object containing the API response
+ * @param objectMapper The ObjectMapper to convert JSON to Java objects
+ * @return A Map containing the device settings
+ * @throws LGThinqApiException If an error occurs during handling the device settings result
+ */
+ protected Map genericHandleDeviceSettingsResult(RestResult resp, ObjectMapper objectMapper)
+ throws LGThinqApiException {
+ Map deviceSettings;
+ Map respMap;
+ String resultCode;
+ if (resp.getStatusCode() != 200) {
+ if (resp.getStatusCode() == 400) {
+ 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.
+ logger.trace(
+ "Error calling device settings from LG Server API. Response is not in json format. Ignoring...",
+ e);
+ }
+ 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