From da97e57aacebcb6680ecda5ad174d35071fc7817 Mon Sep 17 00:00:00 2001 From: Jan Gustafsson Date: Sun, 10 Nov 2024 13:30:04 +0100 Subject: [PATCH] [electroluxappliance] Initial contribution (#17663) * First version Signed-off-by: Jan Gustafsson --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../NOTICE | 13 + .../README.md | 122 +++++ .../pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../ElectroluxApplianceBindingConstants.java | 91 ++++ ...lectroluxApplianceBridgeConfiguration.java | 27 + .../ElectroluxApplianceConfiguration.java | 31 ++ .../ElectroluxApplianceException.java | 47 ++ .../internal/api/ElectroluxGroupAPI.java | 322 ++++++++++++ .../ElectroluxApplianceDiscoveryService.java | 65 +++ .../internal/dto/AirPurifierStateDTO.java | 346 ++++++++++++ .../internal/dto/ApplianceDTO.java | 70 +++ .../internal/dto/ApplianceInfoDTO.java | 84 +++ .../internal/dto/ApplianceStateDTO.java | 46 ++ .../internal/dto/WashingMachineStateDTO.java | 496 ++++++++++++++++++ .../handler/ElectroluxAirPurifierHandler.java | 192 +++++++ .../ElectroluxApplianceBridgeHandler.java | 187 +++++++ .../handler/ElectroluxApplianceHandler.java | 108 ++++ .../ElectroluxApplianceHandlerFactory.java | 77 +++ .../ElectroluxWashingMachineHandler.java | 150 ++++++ .../listener/TokenUpdateListener.java | 31 ++ .../src/main/resources/OH-INF/addon/addon.xml | 11 + .../i18n/electroluxappliance.properties | 171 ++++++ .../resources/OH-INF/thing/thing-types.xml | 450 ++++++++++++++++ bundles/pom.xml | 1 + 27 files changed, 3170 insertions(+) create mode 100644 bundles/org.openhab.binding.electroluxappliance/NOTICE create mode 100644 bundles/org.openhab.binding.electroluxappliance/README.md create mode 100644 bundles/org.openhab.binding.electroluxappliance/pom.xml create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBindingConstants.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceConfiguration.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceException.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/api/ElectroluxGroupAPI.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/discovery/ElectroluxApplianceDiscoveryService.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/AirPurifierStateDTO.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceDTO.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceInfoDTO.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceStateDTO.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/WashingMachineStateDTO.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxAirPurifierHandler.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceBridgeHandler.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandler.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandlerFactory.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxWashingMachineHandler.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/listener/TokenUpdateListener.java create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/i18n/electroluxappliance.properties create mode 100644 bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 9da5cbb79ee..d2acdc2d780 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -96,6 +96,7 @@ /bundles/org.openhab.binding.ecovacs/ @maniac103 /bundles/org.openhab.binding.ecowatt/ @lolodomo /bundles/org.openhab.binding.ekey/ @hmerk +/bundles/org.openhab.binding.electroluxappliance/ @jannegpriv /bundles/org.openhab.binding.elerotransmitterstick/ @vbier /bundles/org.openhab.binding.elroconnects/ @mherwege /bundles/org.openhab.binding.emotiva/ @espenaf diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 3bd18dbc0ba..992b960a5e9 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -471,6 +471,11 @@ org.openhab.binding.ekey ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.electroluxappliance + ${project.version} + org.openhab.addons.bundles org.openhab.binding.elerotransmitterstick diff --git a/bundles/org.openhab.binding.electroluxappliance/NOTICE b/bundles/org.openhab.binding.electroluxappliance/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/NOTICE @@ -0,0 +1,13 @@ +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 diff --git a/bundles/org.openhab.binding.electroluxappliance/README.md b/bundles/org.openhab.binding.electroluxappliance/README.md new file mode 100644 index 00000000000..a1cc8f7f7e2 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/README.md @@ -0,0 +1,122 @@ +# Electrolux Appliance Binding + +This is a binding for Electrolux appliances. + +## Supported Things + +This binding supports the following thing types: + +- api: Bridge - Implements the Electrolux Group API that is used to communicate with the different appliances +- air-purifier: The Electrolux Air Purifier +- washing-machine: The Electrolux Washing Machine + +## Discovery + +After the configuration of the `api` bridge, your Electrolux appliances will be automatically discovered and placed as a thing in the inbox. + +### Configuration Options + +Only the bridge requires manual configuration. +The Electrolux appliance things can be added by hand, or you can let the discovery mechanism automatically find them. + +#### `api` Bridge + +| Parameter | Description | Type | Default | Required | +|--------------|--------------------------------------------------------|--------|----------|----------| +| apiKey | Your created API key on developer.electrolux.one | String | NA | yes | +| refreshToken | Your created refresh token on developer.electrolux.one | String | NA | yes | +| refresh | Specifies the refresh interval in second | Number | 600 | yes | + +#### `air-purifier` Electrolux Air Purifier + +| Parameter | Description | Type | Default | Required | +|--------------|--------------------------------------------------------------------------|--------|----------|----------| +| serialNumber | Serial Number of your Electrolux appliance found in the Electrolux app | Number | NA | yes | + +#### `washing-machine` Electrolux Washing Machine + +| Parameter | Description | Type | Default | Required | +|--------------|--------------------------------------------------------------------------|--------|----------|----------| +| serialNumber | Serial Number of your Electrolux appliance found in the Electrolux app | Number | NA | yes | + +## Channels + +### Electrolux Air Purifier + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|-----------------------------|-----------------------|--------------------------------------------------------------------------------| +| temperature | Number:Temperature | This channel reports the current temperature. | +| humidity | Number:Dimensionless | This channel reports the current humidity in percentage. | +| tvoc | Number:Dimensionless | This channel reports the total Volatile Organic Compounds in ppb. | +| pm1 | Number:Density | This channel reports the Particulate Matter 1 in microgram/m3. | +| pm2_5 | Number:Density | This channel reports the Particulate Matter 2.5 in microgram/m3. | +| pm10 | Number:Density | This channel reports the Particulate Matter 10 in microgram/m3. | +| co2 | Number:Dimensionless | This channel reports the CO2 level in ppm. | +| fan-speed | Number | This channel sets and reports the current fan speed (1-9). | +| filter-life | Number:Dimensionless | This channel reports the remaining filter life in %. | +| ionizer | Switch | This channel sets and reports the status of the Ionizer function (On/Off). | +| door-state | Contact | This channel reports the status of the door (Opened/Closed). | +| work-mode | String | This channel sets and reports the current work mode (Auto, Manual, PowerOff.) | +| ui-light | Switch | This channel sets and reports the status of the UI Light function (On/Off). | +| safety-lock | Switch | This channel sets and reports the status of the Safety Lock function. | +| status | String | This channel is used to fetch latest status from the API. | + +### Electrolux Washing Machine + +The following channels are supported: + +| Channel Type ID | Item Type | Description | +|------------------------------|-----------------------|--------------------------------------------------------------------------------| +| door-state | Contact | This channel reports the status of the door (Opened/Closed). | +| door-lock | Contact | This channel reports the status of the door lock. | +| time-to-start | Number:Time | This channel reports the remaining time for a delayed start washing program. | +| time-to-end | Number:Time | This channel reports the remaining time to the end for a washing program. | +| cycle-phase | String | This channel reports the washing cycle phase. | +| analog-temperature | String | This channel reports the washing temperature. | +| steam-value | String | This channel reports the washing steam value. | +| programs-order | String | This channel reports the washing program. | +| analog-spin-speed | String | This channel reports the washing spin speed. | +| appliance-state | String | This channel reports the appliance state. | +| appliance-mode | String | This channel reports the appliance mode. | +| appliance-total-working-time | Number:Time | This channel reports the total working time for the washing machine. | +| appliance-ui-sw-version | String | This channel reports the appliance UI SW version. | +| optisense-result | String | This channel reports the optisense result. | +| detergent-extradosage | String | This channel reports the detergent extra dosage. | +| softener-extradosage | String | This channel reports the softener extra dosage. | +| water-usage | Number:Volume | This channel reports the water usage in litres. | +| total-wash-cycles-count | Number | This channel reports the total number of washing cycles. | +| status | String | This channel is used to fetch latest status from the API. | + +## Full Example + +### `demo.things` Example + +```java +// Bridge configuration +Bridge electroluxappliance:api:myAPI "Electrolux Group API" [apiKey="12345678", refreshToken="12345678", refresh="300"] { + Thing air-purifier myair-purifier "Electrolux Pure A9" [ serialNummber="123456789" ] +} +``` + +## `demo.items` Example + +```java +// CO2 +Number:Dimensionless electroluxapplianceCO2 "Electrolux Air CO2 [%d ppm]" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:co2"} +// Temperature +Number:Temperature electroluxapplianceTemperature "Electrolux Air Temperature" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:temperature"} +// Door status +Contact electroluxapplianceDoor "Electrolux Air Door Status" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:doorOpen"} +// Work mode +String electroluxapplianceWorkModeSetting "electroluxappliance Work Mode Setting" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:workMode"} +// Fan speed +Number electroluxapplianceFanSpeed "Electrolux Air Fan Speed Setting" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:fanSpeed"} +// UI Light +Switch electroluxapplianceUILight "Electrolux Air UI Light Setting" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:uiLight"} +// Ionizer +Switch electroluxapplianceIonizer "Electrolux Air Ionizer Setting" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:ionizer"} +// Safety Lock +Switch electroluxapplianceSafetyLock "Electrolux Air Safety Lock Setting" {channel="electroluxappliance:air-purifier:myAPI:myair-purifier:safetyLock"} +``` diff --git a/bundles/org.openhab.binding.electroluxappliance/pom.xml b/bundles/org.openhab.binding.electroluxappliance/pom.xml new file mode 100644 index 00000000000..f6875793970 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.electroluxappliance + + openHAB Add-ons :: Bundles :: Electrolux Appliance Binding + + diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/feature/feature.xml b/bundles/org.openhab.binding.electroluxappliance/src/main/feature/feature.xml new file mode 100644 index 00000000000..40075eb948e --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/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.electroluxappliance/${project.version} + + diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBindingConstants.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBindingConstants.java new file mode 100644 index 00000000000..36d5572c90d --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBindingConstants.java @@ -0,0 +1,91 @@ +/** + * 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.electroluxappliance.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link ElectroluxApplianceBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxApplianceBindingConstants { + + public static final String BINDING_ID = "electroluxappliance"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_ELECTROLUX_AIR_PURIFIER = new ThingTypeUID(BINDING_ID, "air-purifier"); + public static final ThingTypeUID THING_TYPE_ELECTROLUX_WASHING_MACHINE = new ThingTypeUID(BINDING_ID, + "washing-machine"); + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "api"); + + // List of all common Channel ids + public static final String CHANNEL_DOOR_STATE = "door-state"; + + // List of all Channel ids for Air Purifers + public static final String CHANNEL_STATUS = "status"; + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_HUMIDITY = "humidity"; + public static final String CHANNEL_TVOC = "tvoc"; + public static final String CHANNEL_PM1 = "pm1"; + public static final String CHANNEL_PM25 = "pm2_5"; + public static final String CHANNEL_PM10 = "pm10"; + public static final String CHANNEL_CO2 = "co2"; + public static final String CHANNEL_FILTER_LIFE = "filter-life"; + public static final String CHANNEL_FAN_SPEED = "fan-speed"; + public static final String CHANNEL_WORK_MODE = "work-mode"; + public static final String CHANNEL_IONIZER = "ionizer"; + public static final String CHANNEL_UI_LIGHT = "ui-light"; + public static final String CHANNEL_SAFETY_LOCK = "safety-lock"; + + // List of all Channel ids for Washing Machines + public static final String CHANNEL_DOOR_LOCK = "door-lock"; + public static final String CHANNEL_TIME_TO_START = "time-to-start"; + public static final String CHANNEL_TIME_TO_END = "time-to-end"; + public static final String CHANNEL_APPLIANCE_UI_SW_VERSION = "appliance-ui-sw-version"; + public static final String CHANNEL_APPLIANCE_TOTAL_WORKING_TIME = "appliance-total-working-time"; + public static final String CHANNEL_APPLIANCE_STATE = "appliance-state"; + public static final String CHANNEL_APPLIANCE_MODE = "appliance-mode"; + public static final String CHANNEL_OPTISENSE_RESULT = "optisense-result"; + public static final String CHANNEL_DETERGENT_EXTRA_DOSAGE = "detergent-extradosage"; + public static final String CHANNEL_SOFTENER_EXTRA_DOSAGE = "softener-extradosage"; + public static final String CHANNEL_WATER_USAGE = "water-usage"; + public static final String CHANNEL_CYCLE_PHASE = "cycle-phase"; + public static final String CHANNEL_TOTAL_WASH_CYCLES_COUNT = "total-wash-cycles-count"; + public static final String CHANNEL_ANALOG_TEMPERATURE = "analog-temperature"; + public static final String CHANNEL_ANALOG_SPIN_SPEED = "analog-spin-speed"; + public static final String CHANNEL_STEAM_VALUE = "steam-value"; + public static final String CHANNEL_PROGRAMS_ORDER = "programs-order"; + + // List of all Properties ids + public static final String PROPERTY_BRAND = "brand"; + public static final String PROPERTY_COLOUR = "colour"; + public static final String PROPERTY_MODEL = "model"; + public static final String PROPERTY_DEVICE = "device"; + public static final String PROPERTY_FW_VERSION = "fwVersion"; + public static final String PROPERTY_SERIAL_NUMBER = "serialNumber"; + public static final String PROPERTY_WORKMODE = "workmode"; + + // List of all Commands for Air Purifiers + public static final String COMMAND_WORKMODE_POWEROFF = "PowerOff"; + public static final String COMMAND_WORKMODE_AUTO = "Auto"; + public static final String COMMAND_WORKMODE_MANUAL = "Manual"; + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, + THING_TYPE_ELECTROLUX_AIR_PURIFIER, THING_TYPE_ELECTROLUX_WASHING_MACHINE); +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBridgeConfiguration.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBridgeConfiguration.java new file mode 100644 index 00000000000..98d57686edc --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceBridgeConfiguration.java @@ -0,0 +1,27 @@ +/** + * 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.electroluxappliance.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ElectroluxApplianceBridgeConfiguration} class contains fields mapping bridge configuration parameters. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxApplianceBridgeConfiguration { + public String apiKey = ""; + public String refreshToken = ""; + public int refresh = 600; +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceConfiguration.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceConfiguration.java new file mode 100644 index 00000000000..814d5b3e060 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.electroluxappliance.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ElectroluxApplianceConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxApplianceConfiguration { + public static final String SERIAL_NUMBER_LABEL = "serialNumber"; + + private String serialNumber = ""; + + public String getSerialNumber() { + return serialNumber; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceException.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceException.java new file mode 100644 index 00000000000..3fe00ef70c4 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/ElectroluxApplianceException.java @@ -0,0 +1,47 @@ +/** + * 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.electroluxappliance.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * {@link ElectroluxApplianceException} is used when there is exception communicating with Electrolux Delta API. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxApplianceException extends Exception { + + private static final long serialVersionUID = 2543564118231301159L; + + public ElectroluxApplianceException(Exception source) { + super(source); + } + + public ElectroluxApplianceException(String message) { + super(message); + } + + @Override + public @Nullable String getMessage() { + Throwable throwable = getCause(); + if (throwable != null) { + String localMessage = throwable.getMessage(); + if (localMessage != null) { + return localMessage; + } + } + return ""; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/api/ElectroluxGroupAPI.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/api/ElectroluxGroupAPI.java new file mode 100644 index 00000000000..5b2366fefd8 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/api/ElectroluxGroupAPI.java @@ -0,0 +1,322 @@ +/** + * 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.electroluxappliance.internal.api; + +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBridgeConfiguration; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceException; +import org.openhab.binding.electroluxappliance.internal.dto.AirPurifierStateDTO; +import org.openhab.binding.electroluxappliance.internal.dto.ApplianceDTO; +import org.openhab.binding.electroluxappliance.internal.dto.ApplianceInfoDTO; +import org.openhab.binding.electroluxappliance.internal.dto.ApplianceStateDTO; +import org.openhab.binding.electroluxappliance.internal.dto.WashingMachineStateDTO; +import org.openhab.binding.electroluxappliance.internal.listener.TokenUpdateListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link ElectroluxGroupAPI} class defines the Elextrolux Group API + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxGroupAPI { + + private static final String BASE_URL = "https://api.developer.electrolux.one"; + private static final String TOKEN_URL = BASE_URL + "/api/v1/token/refresh"; + private static final String APPLIANCES_URL = BASE_URL + "/api/v1/appliances"; + + private static final int MAX_RETRIES = 3; + private static final int REQUEST_TIMEOUT_MS = 10_000; + + private final Logger logger = LoggerFactory.getLogger(ElectroluxGroupAPI.class); + private final Gson gson; + private final HttpClient httpClient; + private final ElectroluxApplianceBridgeConfiguration configuration; + private String accessToken = ""; + private Instant tokenExpiry = Instant.MAX; + private final TokenUpdateListener tokenUpdateListener; + + public ElectroluxGroupAPI(ElectroluxApplianceBridgeConfiguration configuration, Gson gson, HttpClient httpClient, + TokenUpdateListener listener) { + this.gson = gson; + this.configuration = configuration; + this.httpClient = httpClient; + this.tokenUpdateListener = listener; + } + + public boolean refresh(Map electroluxApplianceThings, boolean isCommunicationError) { + try { + if (Instant.now().isAfter(this.tokenExpiry) || isCommunicationError) { + logger.debug("Is communication error: {}", isCommunicationError); + // Refresh since token has expired + refreshToken(); + } else { + logger.debug("Now: {} Token expiry: {}", Instant.now(), this.tokenExpiry); + + } + // Get all appliances + String json = getAppliances(); + ApplianceDTO[] dtos = gson.fromJson(json, ApplianceDTO[].class); + if (dtos != null) { + for (ApplianceDTO dto : dtos) { + String applianceId = dto.getApplianceId(); + // Get appliance info + String jsonApplianceInfo = getApplianceInfo(applianceId); + ApplianceInfoDTO applianceInfo = gson.fromJson(jsonApplianceInfo, ApplianceInfoDTO.class); + if (applianceInfo != null) { + dto.setApplianceInfo(applianceInfo); + if ("AIR_PURIFIER".equals(applianceInfo.getApplianceInfo().getDeviceType())) { + // Get appliance state + String jsonApplianceState = getApplianceState(applianceId); + ApplianceStateDTO applianceState = gson.fromJson(jsonApplianceState, + AirPurifierStateDTO.class); + if (applianceState != null) { + dto.setApplianceState(applianceState); + } + electroluxApplianceThings.put(applianceInfo.getApplianceInfo().getSerialNumber(), dto); + } else if ("WASHING_MACHINE".equals(applianceInfo.getApplianceInfo().getDeviceType())) { + // Get appliance state + String jsonApplianceState = getApplianceState(applianceId); + ApplianceStateDTO applianceState = gson.fromJson(jsonApplianceState, + WashingMachineStateDTO.class); + if (applianceState != null) { + dto.setApplianceState(applianceState); + } + electroluxApplianceThings.put(applianceInfo.getApplianceInfo().getSerialNumber(), dto); + } + } + } + return true; + } + } catch (JsonSyntaxException | ElectroluxApplianceException e) { + logger.warn("Failed to refresh! {}", e.getMessage()); + } + return false; + } + + public boolean workModePowerOff(String applianceId) { + String commandJSON = "{ \"Workmode\": \"PowerOff\" }"; + try { + return sendCommand(commandJSON, applianceId); + } catch (ElectroluxApplianceException e) { + logger.warn("Work mode powerOff failed {}", e.getMessage()); + } + return false; + } + + public boolean workModeAuto(String applianceId) { + String commandJSON = "{ \"Workmode\": \"Auto\" }"; + try { + return sendCommand(commandJSON, applianceId); + } catch (ElectroluxApplianceException e) { + logger.warn("Work mode auto failed {}", e.getMessage()); + } + return false; + } + + public boolean workModeManual(String applianceId) { + String commandJSON = "{ \"Workmode\": \"Manual\" }"; + try { + return sendCommand(commandJSON, applianceId); + } catch (ElectroluxApplianceException e) { + logger.warn("Work mode manual failed {}", e.getMessage()); + } + return false; + } + + public boolean setFanSpeedLevel(String applianceId, int fanSpeedLevel) { + if (fanSpeedLevel < 1 && fanSpeedLevel > 10) { + return false; + } else { + String commandJSON = "{ \"Fanspeed\": " + fanSpeedLevel + "}"; + try { + return sendCommand(commandJSON, applianceId); + } catch (ElectroluxApplianceException e) { + logger.warn("Work mode manual failed {}", e.getMessage()); + } + } + return false; + } + + public boolean setIonizer(String applianceId, String ionizerStatus) { + String commandJSON = "{ \"Ionizer\": " + ionizerStatus + "}"; + try { + return sendCommand(commandJSON, applianceId); + } catch (ElectroluxApplianceException e) { + logger.warn("Work mode manual failed {}", e.getMessage()); + } + return false; + } + + public boolean setUILight(String applianceId, String uiLightStatus) { + String commandJSON = "{ \"UILight\": " + uiLightStatus + "}"; + try { + return sendCommand(commandJSON, applianceId); + } catch (ElectroluxApplianceException e) { + logger.warn("Work mode manual failed {}", e.getMessage()); + } + return false; + } + + public boolean setSafetyLock(String applianceId, String safetyLockStatus) { + String commandJSON = "{ \"SafetyLock\": " + safetyLockStatus + "}"; + try { + return sendCommand(commandJSON, applianceId); + } catch (ElectroluxApplianceException e) { + logger.warn("Work mode manual failed {}", e.getMessage()); + } + return false; + } + + private Request createRequest(String uri, HttpMethod httpMethod) { + Request request = httpClient.newRequest(uri).method(httpMethod); + request.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + request.header(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON); + request.header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON); + + logger.trace("HTTP Request {}.", request.toString()); + + return request; + } + + private void refreshToken() throws ElectroluxApplianceException { + try { + String json = "{\"refreshToken\": \"" + this.configuration.refreshToken + "\"}"; + Request request = createRequest(TOKEN_URL, HttpMethod.POST); + request.content(new StringContentProvider(json), MediaType.APPLICATION_JSON); + logger.debug("HTTP POST Request {}.", request.toString()); + ContentResponse httpResponse; + httpResponse = request.send(); + if (httpResponse.getStatus() != HttpStatus.OK_200) { + throw new ElectroluxApplianceException("Failed to refresh tokens" + httpResponse.getContentAsString()); + } + json = httpResponse.getContentAsString(); + logger.trace("Tokens: {}", json); + JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject(); + this.accessToken = jsonObject.get("accessToken").getAsString(); + this.configuration.refreshToken = jsonObject.get("refreshToken").getAsString(); + // Notify the listener about the updated tokens + tokenUpdateListener.onTokenUpdated(this.configuration.refreshToken); + long expiresIn = jsonObject.get("expiresIn").getAsLong(); + logger.debug("Token expires in: {}s", expiresIn); + this.tokenExpiry = Instant.now().plusSeconds(expiresIn); + logger.debug("Token expires: {}", this.tokenExpiry); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new ElectroluxApplianceException(e); + } + } + + private String getFromApi(String uri) throws ElectroluxApplianceException, InterruptedException { + try { + for (int i = 0; i < MAX_RETRIES; i++) { + try { + Request request = createRequest(uri, HttpMethod.GET); + request.header("x-api-key", this.configuration.apiKey); + request.header(HttpHeader.AUTHORIZATION, "Bearer " + this.accessToken); + logger.trace("Request header {}", request); + + ContentResponse response = request.send(); + String content = response.getContentAsString(); + logger.trace("API response: {}", content); + + if (response.getStatus() != HttpStatus.OK_200) { + logger.debug("getFromApi failed, HTTP status: {}", response.getStatus()); + refreshToken(); + } else { + return content; + } + } catch (TimeoutException e) { + logger.debug("TimeoutException error in get: {}", e.getMessage()); + } + } + throw new ElectroluxApplianceException("Failed to fetch from API!"); + } catch (JsonSyntaxException | ElectroluxApplianceException | ExecutionException e) { + throw new ElectroluxApplianceException(e); + } + } + + private String getAppliances() throws ElectroluxApplianceException { + try { + return getFromApi(APPLIANCES_URL); + } catch (ElectroluxApplianceException | InterruptedException e) { + throw new ElectroluxApplianceException(e); + } + } + + private String getApplianceInfo(String applianceId) throws ElectroluxApplianceException { + try { + return getFromApi(APPLIANCES_URL + "/" + applianceId + "/info"); + } catch (ElectroluxApplianceException | InterruptedException e) { + throw new ElectroluxApplianceException(e); + } + } + + private String getApplianceState(String applianceId) throws ElectroluxApplianceException { + try { + return getFromApi(APPLIANCES_URL + "/" + applianceId + "/state"); + } catch (ElectroluxApplianceException | InterruptedException e) { + throw new ElectroluxApplianceException(e); + } + } + + private boolean sendCommand(String commandJSON, String applianceId) throws ElectroluxApplianceException { + try { + for (int i = 0; i < MAX_RETRIES; i++) { + try { + Request request = createRequest(APPLIANCES_URL + "/" + applianceId + "/command", HttpMethod.PUT); + request.header(HttpHeader.AUTHORIZATION, "Bearer " + this.accessToken); + request.header("x-api-key", this.configuration.apiKey); + request.content(new StringContentProvider(commandJSON), MediaType.APPLICATION_JSON); + logger.trace("Command JSON: {}", commandJSON); + + ContentResponse response = request.send(); + String content = response.getContentAsString(); + logger.trace("API response: {}", content); + + if (response.getStatus() != HttpStatus.OK_200) { + logger.debug("sendCommand failed, HTTP status: {}", response.getStatus()); + refreshToken(); + } else { + return true; + } + } catch (TimeoutException | InterruptedException e) { + logger.warn("TimeoutException error in get"); + } + } + } catch (JsonSyntaxException | ElectroluxApplianceException | ExecutionException e) { + throw new ElectroluxApplianceException(e); + } + return false; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/discovery/ElectroluxApplianceDiscoveryService.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/discovery/ElectroluxApplianceDiscoveryService.java new file mode 100644 index 00000000000..3a3e54159b4 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/discovery/ElectroluxApplianceDiscoveryService.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.electroluxappliance.internal.discovery; + +import static org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceConfiguration; +import org.openhab.binding.electroluxappliance.internal.handler.ElectroluxApplianceBridgeHandler; +import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** + * The {@link ElectroluxApplianceDiscoveryService} searches for available + * Electrolux Pure A9 discoverable through Electrolux Delta API. + * + * @author Jan Gustafsson - Initial contribution + */ +@Component(scope = ServiceScope.PROTOTYPE, service = ElectroluxApplianceDiscoveryService.class) +@NonNullByDefault +public class ElectroluxApplianceDiscoveryService + extends AbstractThingHandlerDiscoveryService { + private static final int SEARCH_TIME = 10; + + public ElectroluxApplianceDiscoveryService() { + super(ElectroluxApplianceBridgeHandler.class, SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME); + } + + @Override + protected void startScan() { + ThingUID bridgeUID = thingHandler.getThing().getUID(); + thingHandler.getElectroluxApplianceThings().entrySet().stream().forEach(thing -> { + String applianceType = thing.getValue().getApplianceType(); + + if ("PUREA9".equalsIgnoreCase(applianceType)) { + thingDiscovered(DiscoveryResultBuilder + .create(new ThingUID(THING_TYPE_ELECTROLUX_AIR_PURIFIER, bridgeUID, thing.getKey())) + .withLabel("Electrolux Pure A9").withBridge(bridgeUID) + .withProperty(ElectroluxApplianceConfiguration.SERIAL_NUMBER_LABEL, thing.getKey()) + .withRepresentationProperty(ElectroluxApplianceConfiguration.SERIAL_NUMBER_LABEL).build()); + } else if ("WM".equalsIgnoreCase(applianceType)) { + thingDiscovered(DiscoveryResultBuilder + .create(new ThingUID(THING_TYPE_ELECTROLUX_WASHING_MACHINE, bridgeUID, thing.getKey())) + .withLabel("Electrolux Washing Machine").withBridge(bridgeUID) + .withProperty(ElectroluxApplianceConfiguration.SERIAL_NUMBER_LABEL, thing.getKey()) + .withRepresentationProperty(ElectroluxApplianceConfiguration.SERIAL_NUMBER_LABEL).build()); + } + }); + + stopScan(); + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/AirPurifierStateDTO.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/AirPurifierStateDTO.java new file mode 100644 index 00000000000..a4790dedb6f --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/AirPurifierStateDTO.java @@ -0,0 +1,346 @@ +/** + * 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.electroluxappliance.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AirPurifierStateDTO} class defines the DTO for the Electrolux Purifiers. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class AirPurifierStateDTO extends ApplianceStateDTO { + + @SerializedName("properties") + private Properties properties = new Properties(); + + public Properties getProperties() { + return properties; + } + + // Inner class for Properties + public static class Properties { + @SerializedName("reported") + private Reported reported = new Reported(); + + public Reported getReported() { + return reported; + } + } + + // Inner class for Reported properties + public static class Reported { + @SerializedName("FrmVer_NIU") + private String frmVerNIU = ""; + + @SerializedName("Workmode") + private String workmode = ""; + + @SerializedName("FilterRFID") + private String filterRFID = ""; + + @SerializedName("FilterLife") + private int filterLife; + + @SerializedName("Fanspeed") + private int fanspeed; + + @SerializedName("UILight") + private boolean uiLight; + + @SerializedName("SafetyLock") + private boolean safetyLock; + + @SerializedName("Ionizer") + private boolean ionizer; + + @SerializedName("Sleep") + private boolean sleep; + + @SerializedName("Scheduler") + private boolean scheduler; + + @SerializedName("FilterType") + private int filterType; + + @SerializedName("DspIcoPM2_5") + private boolean dspIcoPM25; + + @SerializedName("DspIcoPM1") + private boolean dspIcoPM1; + + @SerializedName("DspIcoPM10") + private boolean dspIcoPM10; + + @SerializedName("DspIcoTVOC") + private boolean dspIcoTVOC; + + @SerializedName("ErrPM2_5") + private boolean errPM25; + + @SerializedName("ErrTVOC") + private boolean errTVOC; + + @SerializedName("ErrTempHumidity") + private boolean errTempHumidity; + + @SerializedName("ErrFanMtr") + private boolean errFanMtr; + + @SerializedName("ErrCommSensorDisplayBrd") + private boolean errCommSensorDisplayBrd; + + @SerializedName("DoorOpen") + private boolean doorOpen; + + @SerializedName("ErrRFID") + private boolean errRFID; + + @SerializedName("SignalStrength") + private String signalStrength = ""; + + @SerializedName("logE") + private int logE; + + @SerializedName("logW") + private int logW; + + @SerializedName("InterfaceVer") + private int interfaceVer; + + @SerializedName("VmNo_NIU") + private String vmNoNIU = ""; + + @SerializedName("TVOCBrand") + private String tvocBrand = ""; + + private Capabilities capabilities = new Capabilities(); + + private Tasks tasks = new Tasks(); + + @SerializedName("$version") + private int version; + + private String deviceId = ""; + + @SerializedName("CO2") + private int co2; + + @SerializedName("TVOC") + private int tvoc; + + @SerializedName("Temp") + private int temp; + + @SerializedName("Humidity") + private int humidity; + + @SerializedName("RSSI") + private int rssi; + + @SerializedName("PM1") + private int pm1; + + @SerializedName("PM2_5") + private int pm25; + + @SerializedName("PM10") + private int pm10; + + @SerializedName("ECO2") + private int eco2; + + // Getters for all fields + public String getFrmVerNIU() { + return frmVerNIU; + } + + public String getWorkmode() { + return workmode; + } + + public String getFilterRFID() { + return filterRFID; + } + + public int getFilterLife() { + return filterLife; + } + + public int getFanspeed() { + return fanspeed; + } + + public boolean isUiLight() { + return uiLight; + } + + public boolean isSafetyLock() { + return safetyLock; + } + + public boolean isIonizer() { + return ionizer; + } + + public boolean isSleep() { + return sleep; + } + + public boolean isScheduler() { + return scheduler; + } + + public int getFilterType() { + return filterType; + } + + public boolean isDspIcoPM25() { + return dspIcoPM25; + } + + public boolean isDspIcoPM1() { + return dspIcoPM1; + } + + public boolean isDspIcoPM10() { + return dspIcoPM10; + } + + public boolean isDspIcoTVOC() { + return dspIcoTVOC; + } + + public boolean isErrPM25() { + return errPM25; + } + + public boolean isErrTVOC() { + return errTVOC; + } + + public boolean isErrTempHumidity() { + return errTempHumidity; + } + + public boolean isErrFanMtr() { + return errFanMtr; + } + + public boolean isErrCommSensorDisplayBrd() { + return errCommSensorDisplayBrd; + } + + public boolean isDoorOpen() { + return doorOpen; + } + + public boolean isErrRFID() { + return errRFID; + } + + public String getSignalStrength() { + return signalStrength; + } + + public int getLogE() { + return logE; + } + + public int getLogW() { + return logW; + } + + public int getInterfaceVer() { + return interfaceVer; + } + + public String getVmNoNIU() { + return vmNoNIU; + } + + public String getTvocBrand() { + return tvocBrand; + } + + public Capabilities getCapabilities() { + return capabilities; + } + + public Tasks getTasks() { + return tasks; + } + + public int getVersion() { + return version; + } + + public String getDeviceId() { + return deviceId; + } + + public int getCo2() { + return co2; + } + + public int getTvoc() { + return tvoc; + } + + public int getTemp() { + return temp; + } + + public int getHumidity() { + return humidity; + } + + public int getRssi() { + return rssi; + } + + public int getPm1() { + return pm1; + } + + public int getPm25() { + return pm25; + } + + public int getPm10() { + return pm10; + } + + public int getEco2() { + return eco2; + } + } + + // Inner class for Capabilities + public static class Capabilities { + @SerializedName("tasks") + private Tasks tasks = new Tasks(); + + public Tasks getTasks() { + return tasks; + } + } + + // Inner class for Tasks (assuming it's empty as shown in the JSON) + public static class Tasks { + // No fields; can be extended as needed + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceDTO.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceDTO.java new file mode 100644 index 00000000000..b68a5ad8f6a --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceDTO.java @@ -0,0 +1,70 @@ +/** + * 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.electroluxappliance.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ApplianceDTO} class defines the DTO for the Electrolux Appliances. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ApplianceDTO { + private String applianceId = ""; + private String applianceName = ""; + private String applianceType = ""; + private String created = ""; + private ApplianceInfoDTO applianceInfo = new ApplianceInfoDTO(); + private ApplianceStateDTO applianceState = new ApplianceStateDTO(); + + public void setApplianceInfo(ApplianceInfoDTO applianceInfo) { + this.applianceInfo = applianceInfo; + } + + public void setApplianceState(ApplianceStateDTO applianceState) { + this.applianceState = applianceState; + } + + public ApplianceInfoDTO getApplianceInfo() { + return applianceInfo; + } + + public ApplianceStateDTO getApplianceState() { + return applianceState; + } + + // Getters for each field + public String getApplianceId() { + return applianceId; + } + + public String getApplianceName() { + return applianceName; + } + + public String getApplianceType() { + return applianceType; + } + + public String getCreated() { + return created; + } + + // Optional toString method for easier debugging and logging + @Override + public String toString() { + return "Appliance{" + "applianceId='" + applianceId + '\'' + ", applianceName='" + applianceName + '\'' + + ", applianceType='" + applianceType + '\'' + ", created='" + created + '\'' + '}'; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceInfoDTO.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceInfoDTO.java new file mode 100644 index 00000000000..64b6afd743d --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceInfoDTO.java @@ -0,0 +1,84 @@ +/** + * 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.electroluxappliance.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ApplianceInfoDTO} class defines the DTO for the Electrolux Appliance Info. + * + * @author Jan Gustafsson - Initial contribution + */ + +@NonNullByDefault +public class ApplianceInfoDTO { + + private ApplianceInfo applianceInfo = new ApplianceInfo(); + + // Map capabilities to Object, so details are not parsed + private Object capabilities = new Object(); + + public ApplianceInfo getApplianceInfo() { + return applianceInfo; + } + + public Object getCapabilities() { + return capabilities; + } + + public static class ApplianceInfo { + private String serialNumber = ""; + private String pnc = ""; + private String brand = ""; + private String deviceType = ""; + private String model = ""; + private String variant = ""; + private String colour = ""; + + // Getters + public String getSerialNumber() { + return serialNumber; + } + + public String getPnc() { + return pnc; + } + + public String getBrand() { + return brand; + } + + public String getDeviceType() { + return deviceType; + } + + public String getModel() { + return model; + } + + public String getVariant() { + return variant; + } + + public String getColour() { + return colour; + } + + @Override + public String toString() { + return "ApplianceInfo{" + "serialNumber='" + serialNumber + '\'' + ", pnc='" + pnc + '\'' + ", brand='" + + brand + '\'' + ", deviceType='" + deviceType + '\'' + ", model='" + model + '\'' + ", variant='" + + variant + '\'' + ", colour='" + colour + '\'' + '}'; + } + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceStateDTO.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceStateDTO.java new file mode 100644 index 00000000000..7193c62cd8d --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/ApplianceStateDTO.java @@ -0,0 +1,46 @@ +/** + * 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.electroluxappliance.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ApplianceStateDTO} class defines the DTO for the Electrolux Appliance State. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ApplianceStateDTO { + private String applianceId = ""; + private String connectionState = ""; + private String status = ""; + + public String getApplianceId() { + return applianceId; + } + + public String getConnectionState() { + return connectionState; + } + + public String getStatus() { + return status; + } + + // You can optionally add a toString() method for easier debugging + @Override + public String toString() { + return "ApplianceStateDTO{" + "applianceId='" + applianceId + '\'' + ", connectionState='" + connectionState + + '\'' + ", status='" + status + '\'' + '}'; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/WashingMachineStateDTO.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/WashingMachineStateDTO.java new file mode 100644 index 00000000000..2d02644b6c0 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/dto/WashingMachineStateDTO.java @@ -0,0 +1,496 @@ +/** + * 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.electroluxappliance.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AirPurifierStateDTO} class defines the DTO for the Electrolux Washing Machines. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class WashingMachineStateDTO extends ApplianceStateDTO { + + private Properties properties = new Properties(); + + public Properties getProperties() { + return properties; + } + + public static class Properties { + private Reported reported = new Reported(); + + public Reported getReported() { + return reported; + } + } + + public static class Reported { + private String displayLight = ""; + private String doorState = ""; + private int timeToEnd; + private Miscellaneous miscellaneous = new Miscellaneous(); + private String applianceUiSwVersion = ""; + private int applianceTotalWorkingTime; + private String remoteControl = ""; + private String language = ""; + private FCMiscellaneousState fCMiscellaneousState = new FCMiscellaneousState(); + private String cyclePhase = ""; + private String endOfCycleSound = ""; + private int startTime; + private UserSelections userSelections = new UserSelections(); + private String waterHardness = ""; + private String defaultExtraRinse = ""; + private int totalWashingTime; + private ApplianceInfo applianceInfo = new ApplianceInfo(); + private String doorLock = ""; + private boolean uiLockMode; + private int washingNominalLoadWeight; + private int totalWashCyclesCount; + private int fcOptisenseLoadWeight; + private String waterSoftenerMode = ""; + private String applianceState = ""; + private String applianceMode = ""; + private String applianceMainBoardSwVersion = ""; + private int totalCycleCounter; + private int measuredLoadWeight; + private Object[] alerts = new Object[0]; + private Maintenance applianceCareAndMaintenance0 = new Maintenance(); + private NetworkInterface networkInterface = new NetworkInterface(); + private Maintenance applianceCareAndMaintenance1 = new Maintenance(); + private Maintenance applianceCareAndMaintenance2 = new Maintenance(); + private Maintenance applianceCareAndMaintenance3 = new Maintenance(); + private AutoDosing autoDosing = new AutoDosing(); + private String cycleSubPhase = ""; + private String connectivityState = ""; + + // Getters for all fields + public String getDisplayLight() { + return displayLight; + } + + public String getDoorState() { + return doorState; + } + + public int getTimeToEnd() { + return timeToEnd; + } + + public Miscellaneous getMiscellaneous() { + return miscellaneous; + } + + public String getApplianceUiSwVersion() { + return applianceUiSwVersion; + } + + public int getApplianceTotalWorkingTime() { + return applianceTotalWorkingTime; + } + + public String getRemoteControl() { + return remoteControl; + } + + public String getLanguage() { + return language; + } + + public FCMiscellaneousState getFCMiscellaneousState() { + return fCMiscellaneousState; + } + + public String getCyclePhase() { + return cyclePhase; + } + + public String getEndOfCycleSound() { + return endOfCycleSound; + } + + public int getStartTime() { + return startTime; + } + + public UserSelections getUserSelections() { + return userSelections; + } + + public String getWaterHardness() { + return waterHardness; + } + + public String getDefaultExtraRinse() { + return defaultExtraRinse; + } + + public int getTotalWashingTime() { + return totalWashingTime; + } + + public ApplianceInfo getApplianceInfo() { + return applianceInfo; + } + + public String getDoorLock() { + return doorLock; + } + + public boolean isUiLockMode() { + return uiLockMode; + } + + public int getWashingNominalLoadWeight() { + return washingNominalLoadWeight; + } + + public int getTotalWashCyclesCount() { + return totalWashCyclesCount; + } + + public int getFcOptisenseLoadWeight() { + return fcOptisenseLoadWeight; + } + + public String getWaterSoftenerMode() { + return waterSoftenerMode; + } + + public String getApplianceState() { + return applianceState; + } + + public String getApplianceMode() { + return applianceMode; + } + + public String getApplianceMainBoardSwVersion() { + return applianceMainBoardSwVersion; + } + + public int getTotalCycleCounter() { + return totalCycleCounter; + } + + public int getMeasuredLoadWeight() { + return measuredLoadWeight; + } + + public Object[] getAlerts() { + return alerts; + } + + public Maintenance getApplianceCareAndMaintenance0() { + return applianceCareAndMaintenance0; + } + + public NetworkInterface getNetworkInterface() { + return networkInterface; + } + + public Maintenance getApplianceCareAndMaintenance1() { + return applianceCareAndMaintenance1; + } + + public Maintenance getApplianceCareAndMaintenance2() { + return applianceCareAndMaintenance2; + } + + public Maintenance getApplianceCareAndMaintenance3() { + return applianceCareAndMaintenance3; + } + + public AutoDosing getAutoDosing() { + return autoDosing; + } + + public String getCycleSubPhase() { + return cycleSubPhase; + } + + public String getConnectivityState() { + return connectivityState; + } + } + + public static class Miscellaneous { + private boolean defaultSoftPlus; + + public boolean isDefaultSoftPlus() { + return defaultSoftPlus; + } + } + + public static class FCMiscellaneousState { + private int optisenseResult; + private int detergentExtradosage; + private boolean tankAReserve; + private boolean tankBReserve; + private int softenerExtradosage; + private int waterUsage; + private int tankADetLoadForNominalWeight; + private int tankBDetLoadForNominalWeight; + + public int getOptisenseResult() { + return optisenseResult; + } + + public int getDetergentExtradosage() { + return detergentExtradosage; + } + + public boolean isTankAReserve() { + return tankAReserve; + } + + public boolean isTankBReserve() { + return tankBReserve; + } + + public int getSoftenerExtradosage() { + return softenerExtradosage; + } + + public int getWaterUsage() { + return waterUsage; + } + + public int getTankADetLoadForNominalWeight() { + return tankADetLoadForNominalWeight; + } + + public int getTankBDetLoadForNominalWeight() { + return tankBDetLoadForNominalWeight; + } + } + + public static class UserSelections { + private boolean EWX1493A_ultraMix; + private boolean EWX1493A_stain; + private String adTankBSel = ""; + private String adFineTuneSoftLevel = ""; + private boolean EWX1493A_wetMode; + private String analogSpinSpeed = ""; + private boolean EWX1493A_easyIron; + private boolean EWX1493A_rinseHold; + private boolean EWX1493A_wmEconomy; + private boolean EWX1493A_tcSensor; + private String programUID = ""; + private boolean EWX1493A_anticreaseNoSteam; + private boolean EWX1493A_anticreaseWSteam; + private String adTankASel = ""; + private String timeManagerLevel = ""; + private boolean EWX1493A_dryMode; + private boolean EWX1493A_preWashPhase; + private String extraRinseNumber = ""; + private String adFineTuneDetLevel = ""; + private boolean EWX1493A_steamMode; + private String analogTemperature = ""; + private boolean EWX1493A_nightCycle; + private String steamValue = ""; + private boolean EWX1493A_intensive; + private boolean EWX1493A_pod; + + public boolean isEWX1493A_ultraMix() { + return EWX1493A_ultraMix; + } + + public boolean isEWX1493A_stain() { + return EWX1493A_stain; + } + + public String getAdTankBSel() { + return adTankBSel; + } + + public String getAdFineTuneSoftLevel() { + return adFineTuneSoftLevel; + } + + public boolean isEWX1493A_wetMode() { + return EWX1493A_wetMode; + } + + public String getAnalogSpinSpeed() { + return analogSpinSpeed; + } + + public boolean isEWX1493A_easyIron() { + return EWX1493A_easyIron; + } + + public boolean isEWX1493A_rinseHold() { + return EWX1493A_rinseHold; + } + + public boolean isEWX1493A_wmEconomy() { + return EWX1493A_wmEconomy; + } + + public boolean isEWX1493A_tcSensor() { + return EWX1493A_tcSensor; + } + + public String getProgramUID() { + return programUID; + } + + public boolean isEWX1493A_anticreaseNoSteam() { + return EWX1493A_anticreaseNoSteam; + } + + public boolean isEWX1493A_anticreaseWSteam() { + return EWX1493A_anticreaseWSteam; + } + + public String getAdTankASel() { + return adTankASel; + } + + public String getTimeManagerLevel() { + return timeManagerLevel; + } + + public boolean isEWX1493A_dryMode() { + return EWX1493A_dryMode; + } + + public boolean isEWX1493A_preWashPhase() { + return EWX1493A_preWashPhase; + } + + public String getExtraRinseNumber() { + return extraRinseNumber; + } + + public String getAdFineTuneDetLevel() { + return adFineTuneDetLevel; + } + + public boolean isEWX1493A_steamMode() { + return EWX1493A_steamMode; + } + + public String getAnalogTemperature() { + return analogTemperature; + } + + public boolean isEWX1493A_nightCycle() { + return EWX1493A_nightCycle; + } + + public String getSteamValue() { + return steamValue; + } + + public boolean isEWX1493A_intensive() { + return EWX1493A_intensive; + } + + public boolean isEWX1493A_pod() { + return EWX1493A_pod; + } + } + + public static class ApplianceInfo { + private String applianceType = ""; + + public String getApplianceType() { + return applianceType; + } + } + + public static class Maintenance { + private CareThreshold careThreshold = new CareThreshold(); + + public CareThreshold getCareThreshold() { + return careThreshold; + } + + public static class CareThreshold { + private boolean occurred; + private int threshold; + + public boolean isOccurred() { + return occurred; + } + + public int getThreshold() { + return threshold; + } + } + } + + public static class NetworkInterface { + private String swVersion = ""; + private String linkQualityIndicator = ""; + private String otaState = ""; + private String niuSwUpdateCurrentDescription = ""; + private String swAncAndRevision = ""; + + public String getSwVersion() { + return swVersion; + } + + public String getLinkQualityIndicator() { + return linkQualityIndicator; + } + + public String getOtaState() { + return otaState; + } + + public String getNiuSwUpdateCurrentDescription() { + return niuSwUpdateCurrentDescription; + } + + public String getSwAncAndRevision() { + return swAncAndRevision; + } + } + + public static class AutoDosing { + private int adTankBDetStandardDose; + private boolean adLocalFineTuning; + private String adTankAConfiguration = ""; + private int adTankBSoftStandardDose; + private String adTankBConfiguration = ""; + private int adTankADetStandardDose; + + public int getAdTankBDetStandardDose() { + return adTankBDetStandardDose; + } + + public boolean isAdLocalFineTuning() { + return adLocalFineTuning; + } + + public String getAdTankAConfiguration() { + return adTankAConfiguration; + } + + public int getAdTankBSoftStandardDose() { + return adTankBSoftStandardDose; + } + + public String getAdTankBConfiguration() { + return adTankBConfiguration; + } + + public int getAdTankADetStandardDose() { + return adTankADetStandardDose; + } + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxAirPurifierHandler.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxAirPurifierHandler.java new file mode 100644 index 00000000000..2be88d2f98e --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxAirPurifierHandler.java @@ -0,0 +1,192 @@ +/** + * 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.electroluxappliance.internal.handler; + +import static org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBindingConstants; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceConfiguration; +import org.openhab.binding.electroluxappliance.internal.api.ElectroluxGroupAPI; +import org.openhab.binding.electroluxappliance.internal.dto.AirPurifierStateDTO; +import org.openhab.binding.electroluxappliance.internal.dto.ApplianceDTO; +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.SIUnits; +import org.openhab.core.library.unit.Units; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ElectroluxAirPurifierHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxAirPurifierHandler extends ElectroluxApplianceHandler { + + private final Logger logger = LoggerFactory.getLogger(ElectroluxAirPurifierHandler.class); + + private ElectroluxApplianceConfiguration config = new ElectroluxApplianceConfiguration(); + + public ElectroluxAirPurifierHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Command received: {} on channelID: {}", command, channelUID); + if (CHANNEL_STATUS.equals(channelUID.getId()) || command instanceof RefreshType) { + super.handleCommand(channelUID, command); + } else { + ApplianceDTO dto = getApplianceDTO(); + ElectroluxGroupAPI api = getElectroluxGroupAPI(); + if (api != null && dto != null) { + if (CHANNEL_WORK_MODE.equals(channelUID.getId())) { + if (command.toString().equals(COMMAND_WORKMODE_POWEROFF)) { + api.workModePowerOff(dto.getApplianceId()); + } else if (command.toString().equals(COMMAND_WORKMODE_AUTO)) { + api.workModeAuto(dto.getApplianceId()); + } else if (command.toString().equals(COMMAND_WORKMODE_MANUAL)) { + api.workModeManual(dto.getApplianceId()); + } + } else if (CHANNEL_FAN_SPEED.equals(channelUID.getId())) { + api.setFanSpeedLevel(dto.getApplianceId(), Integer.parseInt(command.toString())); + } else if (CHANNEL_IONIZER.equals(channelUID.getId())) { + if (command == OnOffType.OFF) { + api.setIonizer(dto.getApplianceId(), "false"); + } else if (command == OnOffType.ON) { + api.setIonizer(dto.getApplianceId(), "true"); + } else { + logger.debug("Unknown command! {}", command); + } + } else if (CHANNEL_UI_LIGHT.equals(channelUID.getId())) { + if (command == OnOffType.OFF) { + api.setUILight(dto.getApplianceId(), "false"); + } else if (command == OnOffType.ON) { + api.setUILight(dto.getApplianceId(), "true"); + } else { + logger.debug("Unknown command! {}", command); + } + } else if (CHANNEL_SAFETY_LOCK.equals(channelUID.getId())) { + if (command == OnOffType.OFF) { + api.setSafetyLock(dto.getApplianceId(), "false"); + } else if (command == OnOffType.ON) { + api.setSafetyLock(dto.getApplianceId(), "true"); + } else { + logger.debug("Unknown command! {}", command); + } + } + + final Bridge bridge = getBridge(); + if (bridge != null && bridge.getHandler() instanceof ElectroluxApplianceBridgeHandler bridgeHandler) { + bridgeHandler.handleCommand( + new ChannelUID(this.thing.getUID(), ElectroluxApplianceBindingConstants.CHANNEL_STATUS), + RefreshType.REFRESH); + } + } + } + } + + @Override + public void update(@Nullable ApplianceDTO dto) { + if (dto != null) { + // Update all channels from the updated data + getThing().getChannels().stream().map(Channel::getUID).filter(channelUID -> isLinked(channelUID)) + .forEach(channelUID -> { + State state = getValue(channelUID.getId(), dto); + logger.trace("Channel: {}, State: {}", channelUID, state); + updateState(channelUID, state); + }); + if ("Connected".equalsIgnoreCase(dto.getApplianceState().getConnectionState())) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Air Purifier not connected"); + } + } + } + + private State getValue(String channelId, ApplianceDTO dto) { + var reported = ((AirPurifierStateDTO) dto.getApplianceState()).getProperties().getReported(); + switch (channelId) { + case CHANNEL_TEMPERATURE: + return new QuantityType<>(reported.getTemp(), SIUnits.CELSIUS); + case CHANNEL_HUMIDITY: + return new QuantityType<>(reported.getHumidity(), Units.PERCENT); + case CHANNEL_TVOC: + return new QuantityType<>(reported.getTvoc(), Units.PARTS_PER_BILLION); + case CHANNEL_PM1: + return new QuantityType<>(reported.getPm1(), Units.MICROGRAM_PER_CUBICMETRE); + case CHANNEL_PM25: + return new QuantityType<>(reported.getPm25(), Units.MICROGRAM_PER_CUBICMETRE); + case CHANNEL_PM10: + return new QuantityType<>(reported.getPm10(), Units.MICROGRAM_PER_CUBICMETRE); + case CHANNEL_CO2: + return new QuantityType<>(reported.getCo2(), Units.PARTS_PER_MILLION); + case CHANNEL_FAN_SPEED: + return new StringType(Integer.toString(reported.getFanspeed())); + case CHANNEL_FILTER_LIFE: + return new QuantityType<>(reported.getFilterLife(), Units.PERCENT); + case CHANNEL_IONIZER: + return OnOffType.from(reported.isIonizer()); + case CHANNEL_UI_LIGHT: + return OnOffType.from(reported.isUiLight()); + case CHANNEL_SAFETY_LOCK: + return OnOffType.from(reported.isSafetyLock()); + case CHANNEL_WORK_MODE: + return new StringType(reported.getWorkmode()); + case CHANNEL_DOOR_STATE: + return reported.isDoorOpen() ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + } + return UnDefType.UNDEF; + } + + @Override + public Map refreshProperties() { + Map properties = new HashMap<>(); + + final Bridge bridge = getBridge(); + if (bridge != null && bridge.getHandler() instanceof ElectroluxApplianceBridgeHandler bridgeHandler) { + ApplianceDTO dto = bridgeHandler.getElectroluxApplianceThings().get(config.getSerialNumber()); + if (dto != null) { + var applianceInfo = dto.getApplianceInfo().getApplianceInfo(); + properties.put(Thing.PROPERTY_VENDOR, applianceInfo.getBrand()); + properties.put(PROPERTY_COLOUR, applianceInfo.getColour()); + properties.put(PROPERTY_DEVICE, applianceInfo.getDeviceType()); + properties.put(Thing.PROPERTY_MODEL_ID, applianceInfo.getModel()); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, applianceInfo.getSerialNumber()); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, + ((AirPurifierStateDTO) dto.getApplianceState()).getProperties().getReported().getFrmVerNIU()); + + } + } + return properties; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceBridgeHandler.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceBridgeHandler.java new file mode 100644 index 00000000000..4a0f66897dd --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceBridgeHandler.java @@ -0,0 +1,187 @@ +/** + * 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.electroluxappliance.internal.handler; + +import static org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBindingConstants.*; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBridgeConfiguration; +import org.openhab.binding.electroluxappliance.internal.api.ElectroluxGroupAPI; +import org.openhab.binding.electroluxappliance.internal.discovery.ElectroluxApplianceDiscoveryService; +import org.openhab.binding.electroluxappliance.internal.dto.ApplianceDTO; +import org.openhab.binding.electroluxappliance.internal.listener.TokenUpdateListener; +import org.openhab.core.config.core.Configuration; +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.ThingTypeUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * The {@link ElectroluxApplianceBridgeHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxApplianceBridgeHandler extends BaseBridgeHandler implements TokenUpdateListener { + + private final Logger logger = LoggerFactory.getLogger(ElectroluxApplianceBridgeHandler.class); + + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE); + + private int refreshTimeInSeconds = 300; + private boolean isCommunicationError = false; + + private final Gson gson; + private final HttpClient httpClient; + private final Map electroluxApplianceThings = new ConcurrentHashMap<>(); + + private @Nullable ElectroluxGroupAPI api; + private @Nullable ScheduledFuture refreshJob; + private @Nullable ScheduledFuture instantUpdate; + + public ElectroluxApplianceBridgeHandler(Bridge bridge, HttpClient httpClient, Gson gson) { + super(bridge); + this.httpClient = httpClient; + this.gson = gson; + } + + @Override + public void initialize() { + ElectroluxApplianceBridgeConfiguration config = getConfigAs(ElectroluxApplianceBridgeConfiguration.class); + + refreshTimeInSeconds = config.refresh; + + if (config.apiKey.isBlank() || config.refreshToken.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Configuration of API key, access and refresh token is mandatory"); + } else if (refreshTimeInSeconds < 10) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Refresh time cannot be less than 10!"); + } else { + try { + this.api = new ElectroluxGroupAPI(config, gson, httpClient, this); + scheduler.execute(() -> { + updateStatus(ThingStatus.UNKNOWN); + startAutomaticRefresh(); + }); + } catch (RuntimeException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + } + + @Override + public void onTokenUpdated(@Nullable String newRefreshToken) { + // Create a new configuration object with the updated tokens + Configuration configuration = editConfiguration(); + configuration.put("refreshToken", newRefreshToken); + // Update the configuration + updateConfiguration(configuration); + } + + public Map getElectroluxApplianceThings() { + return electroluxApplianceThings; + } + + @Override + public Collection> getServices() { + return Set.of(ElectroluxApplianceDiscoveryService.class); + } + + @Override + public void dispose() { + stopAutomaticRefresh(); + } + + public @Nullable ElectroluxGroupAPI getElectroluxDeltaAPI() { + return api; + } + + private boolean refreshAndUpdateStatus() { + if (api != null) { + if (api.refresh(electroluxApplianceThings, isCommunicationError)) { + getThing().getThings().stream().forEach(thing -> { + ElectroluxApplianceHandler handler = (ElectroluxApplianceHandler) thing.getHandler(); + if (handler != null) { + handler.update(); + } + }); + updateStatus(ThingStatus.ONLINE); + isCommunicationError = false; + return true; + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + isCommunicationError = true; + } + } + return false; + } + + private void startAutomaticRefresh() { + ScheduledFuture refreshJob = this.refreshJob; + if (refreshJob == null || refreshJob.isCancelled()) { + this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshAndUpdateStatus, 0, refreshTimeInSeconds, + TimeUnit.SECONDS); + } + } + + private void stopAutomaticRefresh() { + ScheduledFuture refreshJob = this.refreshJob; + if (refreshJob != null) { + refreshJob.cancel(true); + this.refreshJob = null; + } + refreshJob = this.instantUpdate; + if (refreshJob != null) { + refreshJob.cancel(true); + this.refreshJob = null; + } + } + + private synchronized void updateNow() { + Future localRef = instantUpdate; + if (localRef == null || localRef.isDone()) { + instantUpdate = scheduler.schedule(this::refreshAndUpdateStatus, 0, TimeUnit.SECONDS); + } else { + logger.debug("Already waiting for scheduled refresh"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Command received: {} on channelID: {}", command, channelUID); + if (CHANNEL_STATUS.equals(channelUID.getId()) || command instanceof RefreshType) { + updateNow(); + } + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandler.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandler.java new file mode 100644 index 00000000000..af599de2ab2 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandler.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.electroluxappliance.internal.handler; + +import static org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBindingConstants.CHANNEL_STATUS; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceConfiguration; +import org.openhab.binding.electroluxappliance.internal.api.ElectroluxGroupAPI; +import org.openhab.binding.electroluxappliance.internal.dto.ApplianceDTO; +import org.openhab.core.thing.Bridge; +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.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ElectroluxApplianceHandler} is + * + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public abstract class ElectroluxApplianceHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(ElectroluxApplianceHandler.class); + + private ElectroluxApplianceConfiguration config = new ElectroluxApplianceConfiguration(); + + public ElectroluxApplianceHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Command received: {} on channelID: {}", command, channelUID); + if (CHANNEL_STATUS.equals(channelUID.getId()) || command instanceof RefreshType) { + final Bridge bridge = getBridge(); + if (bridge != null && bridge.getHandler() instanceof ElectroluxApplianceBridgeHandler bridgeHandler) { + bridgeHandler.handleCommand(channelUID, command); + } + } + } + + @Override + public void initialize() { + config = getConfigAs(ElectroluxApplianceConfiguration.class); + if (config.getSerialNumber().isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Configuration of Serial Number is mandatory"); + } else { + updateStatus(ThingStatus.UNKNOWN); + + scheduler.execute(() -> { + update(); + Map properties = refreshProperties(); + updateProperties(properties); + }); + } + } + + public void update() { + ApplianceDTO dto = getApplianceDTO(); + if (dto != null) { + update(dto); + } else { + logger.warn("AppliancedDTO is null!"); + } + } + + protected @Nullable ElectroluxGroupAPI getElectroluxGroupAPI() { + final Bridge bridge = getBridge(); + if (bridge != null && bridge.getHandler() instanceof ElectroluxApplianceBridgeHandler bridgeHandler) { + return bridgeHandler.getElectroluxDeltaAPI(); + } + return null; + } + + protected @Nullable ApplianceDTO getApplianceDTO() { + final Bridge bridge = getBridge(); + if (bridge != null && bridge.getHandler() instanceof ElectroluxApplianceBridgeHandler bridgeHandler) { + return bridgeHandler.getElectroluxApplianceThings().get(config.getSerialNumber()); + } + return null; + } + + public abstract void update(@Nullable ApplianceDTO dto); + + public abstract Map refreshProperties(); +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandlerFactory.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandlerFactory.java new file mode 100644 index 00000000000..68d2ee1cd82 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxApplianceHandlerFactory.java @@ -0,0 +1,77 @@ +/** + * 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.electroluxappliance.internal.handler; + +import static org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +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.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +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; + +import com.google.gson.Gson; + +/** + * The {@link ElectroluxApplianceHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.electroluxappliance", service = ThingHandlerFactory.class) +public class ElectroluxApplianceHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ELECTROLUX_AIR_PURIFIER, + THING_TYPE_ELECTROLUX_WASHING_MACHINE, THING_TYPE_BRIDGE); + private final Gson gson; + private HttpClient httpClient; + private final Logger logger = LoggerFactory.getLogger(ElectroluxApplianceHandlerFactory.class); + + @Activate + public ElectroluxApplianceHandlerFactory(@Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.gson = new Gson(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_ELECTROLUX_AIR_PURIFIER.equals(thingTypeUID)) { + return new ElectroluxAirPurifierHandler(thing); + } else if (THING_TYPE_ELECTROLUX_WASHING_MACHINE.equals(thingTypeUID)) { + return new ElectroluxWashingMachineHandler(thing); + } else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new ElectroluxApplianceBridgeHandler((Bridge) thing, httpClient, gson); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxWashingMachineHandler.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxWashingMachineHandler.java new file mode 100644 index 00000000000..ae598ace6f1 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/handler/ElectroluxWashingMachineHandler.java @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.electroluxappliance.internal.handler; + +import static org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.electroluxappliance.internal.ElectroluxApplianceConfiguration; +import org.openhab.binding.electroluxappliance.internal.dto.ApplianceDTO; +import org.openhab.binding.electroluxappliance.internal.dto.WashingMachineStateDTO; +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.Units; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ElectroluxWashingMachineHandler} is responsible for handling commands and status updates for + * Electrolux washing machines. + * + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class ElectroluxWashingMachineHandler extends ElectroluxApplianceHandler { + + private final Logger logger = LoggerFactory.getLogger(ElectroluxWashingMachineHandler.class); + + private ElectroluxApplianceConfiguration config = new ElectroluxApplianceConfiguration(); + + public ElectroluxWashingMachineHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Command received: {} on channelID: {}", command, channelUID); + if (CHANNEL_STATUS.equals(channelUID.getId()) || command instanceof RefreshType) { + super.handleCommand(channelUID, command); + } + } + + @Override + public void update(@Nullable ApplianceDTO dto) { + if (dto != null) { + // Update all channels from the updated data + getThing().getChannels().stream().map(Channel::getUID).filter(channelUID -> isLinked(channelUID)) + .forEach(channelUID -> { + State state = getValue(channelUID.getId(), dto); + logger.trace("Channel: {}, State: {}", channelUID, state); + updateState(channelUID, state); + }); + if ("Connected".equalsIgnoreCase(dto.getApplianceState().getConnectionState())) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Washing Machine not connected"); + } + } + } + + private State getValue(String channelId, ApplianceDTO dto) { + var reported = ((WashingMachineStateDTO) dto.getApplianceState()).getProperties().getReported(); + switch (channelId) { + case CHANNEL_DOOR_STATE: + return "OPEN".equals(reported.getDoorState()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + case CHANNEL_DOOR_LOCK: + return "ON".equals(reported.getDoorLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + case CHANNEL_TIME_TO_START: + return new QuantityType<>(reported.getStartTime(), Units.SECOND); + case CHANNEL_TIME_TO_END: + return new QuantityType<>(reported.getTimeToEnd(), Units.SECOND); + case CHANNEL_APPLIANCE_UI_SW_VERSION: + return new StringType(reported.getApplianceUiSwVersion()); + case CHANNEL_OPTISENSE_RESULT: + return new StringType(Integer.toString(reported.getFCMiscellaneousState().getOptisenseResult())); + case CHANNEL_DETERGENT_EXTRA_DOSAGE: + return new StringType(Integer.toString(reported.getFCMiscellaneousState().getDetergentExtradosage())); + case CHANNEL_SOFTENER_EXTRA_DOSAGE: + return new StringType(Integer.toString(reported.getFCMiscellaneousState().getSoftenerExtradosage())); + case CHANNEL_WATER_USAGE: + return new QuantityType<>(reported.getFCMiscellaneousState().getWaterUsage(), Units.LITRE); + case CHANNEL_TOTAL_WASH_CYCLES_COUNT: + return new StringType(Integer.toString(reported.getTotalWashCyclesCount())); + case CHANNEL_CYCLE_PHASE: + return new StringType(reported.getCyclePhase()); + case CHANNEL_APPLIANCE_TOTAL_WORKING_TIME: + return new StringType(Integer.toString(reported.getApplianceTotalWorkingTime())); + case CHANNEL_APPLIANCE_STATE: + return new StringType(reported.getApplianceState()); + case CHANNEL_APPLIANCE_MODE: + return new StringType(reported.getApplianceMode()); + case CHANNEL_ANALOG_TEMPERATURE: + return new StringType(reported.getUserSelections().getAnalogTemperature()); + case CHANNEL_ANALOG_SPIN_SPEED: + return new StringType(reported.getUserSelections().getAnalogSpinSpeed()); + case CHANNEL_STEAM_VALUE: + return new StringType(reported.getUserSelections().getSteamValue()); + case CHANNEL_PROGRAMS_ORDER: + return new StringType(reported.getUserSelections().getProgramUID()); + + } + return UnDefType.UNDEF; + } + + @Override + public Map refreshProperties() { + Map properties = new HashMap<>(); + final Bridge bridge = getBridge(); + if (bridge != null && bridge.getHandler() instanceof ElectroluxApplianceBridgeHandler bridgeHandler) { + ApplianceDTO dto = bridgeHandler.getElectroluxApplianceThings().get(config.getSerialNumber()); + if (dto != null) { + var applianceInfo = dto.getApplianceInfo().getApplianceInfo(); + properties.put(Thing.PROPERTY_VENDOR, applianceInfo.getBrand()); + properties.put(PROPERTY_COLOUR, applianceInfo.getColour()); + properties.put(PROPERTY_DEVICE, applianceInfo.getDeviceType()); + properties.put(Thing.PROPERTY_MODEL_ID, applianceInfo.getModel()); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, applianceInfo.getSerialNumber()); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, ((WashingMachineStateDTO) dto.getApplianceState()) + .getProperties().getReported().getNetworkInterface().getSwVersion()); + } + } + return properties; + } +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/listener/TokenUpdateListener.java b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/listener/TokenUpdateListener.java new file mode 100644 index 00000000000..4362c4e9778 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/java/org/openhab/binding/electroluxappliance/internal/listener/TokenUpdateListener.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.electroluxappliance.internal.listener; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TokenUpdateListener} callback interface for notifying about token updates + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public interface TokenUpdateListener { + /** + * Called when the access token and refresh token are updated. + * + * @param newAccessToken the new access token + * @param newRefreshToken the new refresh token + */ + void onTokenUpdated(String newRefreshToken); +} diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..ea9b1b3f653 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + Electrolux Appliances Binding + This is the binding for Electrolux Appliances. + cloud + + diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/i18n/electroluxappliance.properties b/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/i18n/electroluxappliance.properties new file mode 100644 index 00000000000..53801fd00e0 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/i18n/electroluxappliance.properties @@ -0,0 +1,171 @@ +# add-on + +addon.electroluxappliance.name = Electrolux Appliances Binding +addon.electroluxappliance.description = This is the binding for Electrolux Appliances. + +# thing types + +thing-type.electroluxappliance.air-purifier.label = Air Purifier +thing-type.electroluxappliance.air-purifier.description = This thing represents the Electrolux Air Purifier. +thing-type.electroluxappliance.api.label = Electrolux Group API +thing-type.electroluxappliance.api.description = This bridge represents the web API connector. +thing-type.electroluxappliance.washing-machine.label = Washing Machine +thing-type.electroluxappliance.washing-machine.description = This thing represents the Electrolux Washing Machine. + +# thing types config + +thing-type.config.electroluxappliance.air-purifier.serialNumber.label = Serial Number +thing-type.config.electroluxappliance.air-purifier.serialNumber.description = The appliance serial number. +thing-type.config.electroluxappliance.api.apiKey.label = API Key +thing-type.config.electroluxappliance.api.apiKey.description = Your personal API key. +thing-type.config.electroluxappliance.api.refresh.label = Refresh Interval +thing-type.config.electroluxappliance.api.refresh.description = Specifies the refresh interval in seconds. +thing-type.config.electroluxappliance.api.refreshToken.label = Refresh Token +thing-type.config.electroluxappliance.api.refreshToken.description = Your personal Refresh Token. +thing-type.config.electroluxappliance.washing-machine.serialNumber.label = Serial Number +thing-type.config.electroluxappliance.washing-machine.serialNumber.description = The appliance serial number. + +# channel types + +channel-type.electroluxappliance.analog-spin-speed.label = Spin Speed +channel-type.electroluxappliance.analog-spin-speed.description = The spin speed. +channel-type.electroluxappliance.analog-spin-speed.state.option.DISABLED = Disabled +channel-type.electroluxappliance.analog-spin-speed.state.option.0_RPM = 0 rpm +channel-type.electroluxappliance.analog-spin-speed.state.option.400_RPM = 400 rpm +channel-type.electroluxappliance.analog-spin-speed.state.option.600_RPM = 600 rpm +channel-type.electroluxappliance.analog-spin-speed.state.option.800_RPM = 800 rpm +channel-type.electroluxappliance.analog-spin-speed.state.option.1000_RPM = 1000 rpm +channel-type.electroluxappliance.analog-spin-speed.state.option.1200_RPM = 1200 rpm +channel-type.electroluxappliance.analog-spin-speed.state.option.1400_RPM = 1400 rpm +channel-type.electroluxappliance.analog-spin-speed.state.option.1600_RPM = 1600 rpm +channel-type.electroluxappliance.analog-temperature.label = Washing Temperature +channel-type.electroluxappliance.analog-temperature.description = The user configured washing temperature. +channel-type.electroluxappliance.analog-temperature.state.option.20_CELSIUS = 20°C +channel-type.electroluxappliance.analog-temperature.state.option.30_CELSIUS = 30°C +channel-type.electroluxappliance.analog-temperature.state.option.40_CELSIUS = 40°C +channel-type.electroluxappliance.analog-temperature.state.option.50_CELSIUS = 50°C +channel-type.electroluxappliance.analog-temperature.state.option.60_CELSIUS = 60°C +channel-type.electroluxappliance.analog-temperature.state.option.90_CELSIUS = 90°C +channel-type.electroluxappliance.analog-temperature.state.option.COLD = Cold +channel-type.electroluxappliance.appliance-mode.label = Appliance Mode +channel-type.electroluxappliance.appliance-mode.description = The appliance mode. +channel-type.electroluxappliance.appliance-mode.state.option.DEMO = Demo +channel-type.electroluxappliance.appliance-mode.state.option.DIAGNOSTIC = Diagnostic +channel-type.electroluxappliance.appliance-mode.state.option.NORMAL = Normal +channel-type.electroluxappliance.appliance-mode.state.option.SERVICE = Service +channel-type.electroluxappliance.appliance-state.label = Appliance State +channel-type.electroluxappliance.appliance-state.description = The appliance state. +channel-type.electroluxappliance.appliance-state.state.option.ALARM = Alarm +channel-type.electroluxappliance.appliance-state.state.option.DELAYED_START = Delayed start +channel-type.electroluxappliance.appliance-state.state.option.END_OF_CYCLE = End of cycle +channel-type.electroluxappliance.appliance-state.state.option.IDLE = Idle +channel-type.electroluxappliance.appliance-state.state.option.OFF = Off +channel-type.electroluxappliance.appliance-state.state.option.PAUSED = Paused +channel-type.electroluxappliance.appliance-state.state.option.READY_TO_START = Ready to start +channel-type.electroluxappliance.appliance-state.state.option.RUNNING = Running +channel-type.electroluxappliance.appliance-total-working-time.label = Appliance Total Working Time +channel-type.electroluxappliance.appliance-total-working-time.description = The appliance total working time. +channel-type.electroluxappliance.appliance-ui-sw-version.label = Appliance UI SW Version +channel-type.electroluxappliance.appliance-ui-sw-version.description = The appliance UI SW version. +channel-type.electroluxappliance.co2.label = CO2 +channel-type.electroluxappliance.co2.description = The measured CarbonDioxide. +channel-type.electroluxappliance.cycle-phase.label = Cycle Phase +channel-type.electroluxappliance.cycle-phase.description = The washing cycle phase. +channel-type.electroluxappliance.cycle-phase.state.option.UNAVAILABLE = Unavailable +channel-type.electroluxappliance.cycle-phase.state.option.ANTICREASE = Anicrease +channel-type.electroluxappliance.cycle-phase.state.option.DRAIN = Drain +channel-type.electroluxappliance.cycle-phase.state.option.DRY = Dry +channel-type.electroluxappliance.cycle-phase.state.option.PREWASH = Prewash +channel-type.electroluxappliance.cycle-phase.state.option.RINSE = Rinse +channel-type.electroluxappliance.cycle-phase.state.option.SPIN = Spin +channel-type.electroluxappliance.cycle-phase.state.option.STEAM = Steam +channel-type.electroluxappliance.cycle-phase.state.option.WASH = Wash +channel-type.electroluxappliance.detergent-extradosage.label = Detergent Extra Dosage +channel-type.electroluxappliance.detergent-extradosage.description = The detergent extra dosage. +channel-type.electroluxappliance.door-lock.label = Door Lock +channel-type.electroluxappliance.door-lock.description = The door lock status Open/Closed. +channel-type.electroluxappliance.door-state.label = Door State +channel-type.electroluxappliance.door-state.description = The door status Open/Closed. +channel-type.electroluxappliance.fan-speed.label = Fan Speed Setting +channel-type.electroluxappliance.fan-speed.description = The fan speed setting. +channel-type.electroluxappliance.fan-speed.state.option.1 = Level 1 +channel-type.electroluxappliance.fan-speed.state.option.2 = Level 2 +channel-type.electroluxappliance.fan-speed.state.option.3 = Level 3 +channel-type.electroluxappliance.fan-speed.state.option.4 = Level 4 +channel-type.electroluxappliance.fan-speed.state.option.5 = Level 5 +channel-type.electroluxappliance.fan-speed.state.option.6 = Level 6 +channel-type.electroluxappliance.fan-speed.state.option.7 = Level 7 +channel-type.electroluxappliance.fan-speed.state.option.8 = Level 8 +channel-type.electroluxappliance.fan-speed.state.option.9 = Level 9 +channel-type.electroluxappliance.filter-life.label = Remaining Filter Life +channel-type.electroluxappliance.filter-life.description = The remaining filter life indication in percent. +channel-type.electroluxappliance.humidity.label = Humidity +channel-type.electroluxappliance.humidity.description = The measured humidity. +channel-type.electroluxappliance.ionizer.label = Ionizer Status +channel-type.electroluxappliance.ionizer.description = The ionizer status On/Off. +channel-type.electroluxappliance.optisense-result.label = Optisense Result +channel-type.electroluxappliance.optisense-result.description = The optisense result. +channel-type.electroluxappliance.pm1.label = PM1 +channel-type.electroluxappliance.pm1.description = The Particulate Matter 1 (0.001mm). +channel-type.electroluxappliance.pm10.label = PM10 +channel-type.electroluxappliance.pm10.description = The Particulate Matter 10 (0.01mm). +channel-type.electroluxappliance.pm2_5.label = PM2.5 +channel-type.electroluxappliance.pm2_5.description = The Particulate Matter 2.5 (0.0025mm). +channel-type.electroluxappliance.programs-order.label = Programs Order +channel-type.electroluxappliance.programs-order.description = The user configured washing program. +channel-type.electroluxappliance.programs-order.state.option.MACHINE_SETTINGS_HIDDEN_TEST = Machine settings hidden test +channel-type.electroluxappliance.programs-order.state.option.COTTON_PR_ECO40-60 = Cotton Eco 40°C-60°C +channel-type.electroluxappliance.programs-order.state.option.COTTON_PR_COTTONS = Cotton +channel-type.electroluxappliance.programs-order.state.option.SYNTHETIC_PR_SYNTHETICS = Synthetics +channel-type.electroluxappliance.programs-order.state.option.DELICATE_PR_DELICATES = Delicates +channel-type.electroluxappliance.programs-order.state.option.WOOL_PR_WOOL_SILK = Wool Silk +channel-type.electroluxappliance.programs-order.state.option.STEAM_REFRESH_PR_STEAMFRESHSCENT = Steam fresh cent +channel-type.electroluxappliance.programs-order.state.option.SPIN_PR_DRAIN_SPIN = Drain spin +channel-type.electroluxappliance.programs-order.state.option.SOFTENER_PR_RINSE = Softener rinse +channel-type.electroluxappliance.programs-order.state.option.QUICK_20_MIN_PR_RAPID20MIN = Rapid 20min +channel-type.electroluxappliance.programs-order.state.option.SPORT_JACKETS_PR_OUTDOOR = Sport jackets outdoor +channel-type.electroluxappliance.programs-order.state.option.SYNTHETIC_PR_SPORT = Synthetic sport +channel-type.electroluxappliance.programs-order.state.option.DENIM_PR_DENIM = Denim +channel-type.electroluxappliance.programs-order.state.option.BLANKET_PR_DUVET = Blanket duvet +channel-type.electroluxappliance.programs-order.state.option.SANITISE60_PR_ANTIALLERGYVAPOUR = Anti allergy vapour +channel-type.electroluxappliance.programs-order.state.option.COTTON_PR_BUSINESSSHIRTS = Cotton business shirts +channel-type.electroluxappliance.programs-order.state.option.DRUM_CLEAN_PR_MACHINECLEAN = Drum machine clean +channel-type.electroluxappliance.programs-order.state.option.STEAM_DEWRINKLER_PR_STEAMCASHMERE = Steam cashmere +channel-type.electroluxappliance.programs-order.state.option.SYNTHETIC_PR_BEDLINEN = Synthetic bedlinen +channel-type.electroluxappliance.programs-order.state.option.COTTON_PR_TOWELS = Cotton towels +channel-type.electroluxappliance.programs-order.state.option.DELICATE_PR_CURTAINS = Delicate curtains +channel-type.electroluxappliance.programs-order.state.option.SYNTHETIC_PR_FLEECE = Synthetic fleece +channel-type.electroluxappliance.programs-order.state.option.COTTON_PR_WORKINGCLOTHES = Cotton working clothes +channel-type.electroluxappliance.programs-order.state.option.SYNTHETIC_PR_MICROFIBRE = Synthetic microfibre +channel-type.electroluxappliance.programs-order.state.option.DELICATE_PR_BABY = Delicate baby +channel-type.electroluxappliance.safety-lock.label = Safety Lock +channel-type.electroluxappliance.safety-lock.description = The safety lock status. +channel-type.electroluxappliance.softener-extradosage.label = Softener Extra Dosage +channel-type.electroluxappliance.softener-extradosage.description = The softener extra dosage. +channel-type.electroluxappliance.status.label = Fetch Current Status +channel-type.electroluxappliance.status.description = Used to fetch latest status from API. +channel-type.electroluxappliance.steam-value.label = Steam Value +channel-type.electroluxappliance.steam-value.description = The user configured steam value. +channel-type.electroluxappliance.steam-value.state.option.STEAM_OFF = Steam off +channel-type.electroluxappliance.steam-value.state.option.STEAM_MIN = Steam minumum +channel-type.electroluxappliance.steam-value.state.option.STEAM_MED = Steam medium +channel-type.electroluxappliance.steam-value.state.option.STEAM_MAX = Steam max +channel-type.electroluxappliance.temperature.label = Temperature +channel-type.electroluxappliance.temperature.description = The measured temperature. +channel-type.electroluxappliance.time-to-end.label = Time To End +channel-type.electroluxappliance.time-to-end.description = The time remaining until the program will end. +channel-type.electroluxappliance.time-to-start.label = Time To Delayed Start +channel-type.electroluxappliance.time-to-start.description = The time remaining until the delayed start. +channel-type.electroluxappliance.total-wash-cycles-count.label = Total Wash Cycles Count +channel-type.electroluxappliance.total-wash-cycles-count.description = The total wash cycles count. +channel-type.electroluxappliance.tvoc.label = TVOC +channel-type.electroluxappliance.tvoc.description = The total Volatile Organic Compounds. +channel-type.electroluxappliance.ui-light.label = UI Light +channel-type.electroluxappliance.ui-light.description = The air quality light status indication. +channel-type.electroluxappliance.water-usage.label = Water Usage +channel-type.electroluxappliance.water-usage.description = The water usage in litres. +channel-type.electroluxappliance.work-mode.label = Work Mode Setting +channel-type.electroluxappliance.work-mode.description = The work mode setting. +channel-type.electroluxappliance.work-mode.state.option.PowerOff = Power Off +channel-type.electroluxappliance.work-mode.state.option.Auto = Automatic +channel-type.electroluxappliance.work-mode.state.option.Manual = Manual diff --git a/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..b8219a94577 --- /dev/null +++ b/bundles/org.openhab.binding.electroluxappliance/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,450 @@ + + + + + + This bridge represents the web API connector. + + + Electrolux + + + + + + Your personal API key. + + + + Your personal Refresh Token. + + + + Specifies the refresh interval in seconds. + 300 + + + + + + + + + + + This thing represents the Electrolux Air Purifier. + + + + + + + + + + + + + + + + + + + + + Electrolux + + + serialNumber + + + + + The appliance serial number. + + + + + + + + + + + + This thing represents the Electrolux Washing Machine. + + + + + + + + + + + + + + + + + + + + + + + + + Electrolux + + + serialNumber + + + + + The appliance serial number. + + + + + + + + String + + Used to fetch latest status from API. + + + + + Number:Temperature + + The measured temperature. + Temperature + + + + + + Number:Dimensionless + + The measured humidity. + Humidity + + + + + Number:Dimensionless + + The total Volatile Organic Compounds. + + + + + + Number:Density + + The Particulate Matter 1 (0.001mm). + + + + + Number:Density + + The Particulate Matter 2.5 (0.0025mm). + + + + + Number:Density + + The Particulate Matter 10 (0.01mm). + + + + + Number:Dimensionless + + The measured CarbonDioxide. + + + + + Number:Dimensionless + + The remaining filter life indication in percent. + + + + + Number + + The fan speed setting. + + + + + + + + + + + + + + + + + String + + The work mode setting. + + + + + + + + + + + Switch + + The ionizer status On/Off. + + + + Switch + + The air quality light status indication. + + + + Switch + + The safety lock status. + + + + Contact + + The door lock status Open/Closed. + + + + + Contact + + The door status Open/Closed. + + + + + Number:Time + + The time remaining until the delayed start. + + + + + Number:Time + + The time remaining until the program will end. + + + + + String + + The appliance UI SW version. + + + + + Number:Time + + The appliance total working time. + + + + + Number + + The optisense result. + + + + + Number + + The detergent extra dosage. + + + + + Number + + The softener extra dosage. + + + + + Number:Volume + + The water usage in litres. + + + + + Number + + The total wash cycles count. + + + + + String + + The washing cycle phase. + + + + + + + + + + + + + + + + + String + + The appliance state. + + + + + + + + + + + + + + + + String + + The appliance mode. + + + + + + + + + + + + String + + The user configured washing temperature. + + + + + + + + + + + + + + + + String + + The spin speed. + + + + + + + + + + + + + + + + + String + + The user configured steam value. + + + + + + + + + + + + String + + The user configured washing program. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index b3a95a4c86b..95995ea9d32 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -130,6 +130,7 @@ org.openhab.binding.ecovacs org.openhab.binding.ecowatt org.openhab.binding.ekey + org.openhab.binding.electroluxappliance org.openhab.binding.elerotransmitterstick org.openhab.binding.elroconnects org.openhab.binding.emotiva