diff --git a/CODEOWNERS b/CODEOWNERS
index 4be0fa8c6..239360141 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -80,6 +80,7 @@
/bundles/org.openhab.binding.ecobee/ @mhilbush
/bundles/org.openhab.binding.ecotouch/ @sibbi77
/bundles/org.openhab.binding.ekey/ @hmerk
+/bundles/org.openhab.binding.electroluxair/ @jannegpriv
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
/bundles/org.openhab.binding.energenie/ @hmerk
/bundles/org.openhab.binding.enigma2/ @gdolfen
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index caa29bac5..4e06be806 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -391,6 +391,11 @@
org.openhab.binding.ekey
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.electroluxair
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.elerotransmitterstick
diff --git a/bundles/org.openhab.binding.electroluxair/NOTICE b/bundles/org.openhab.binding.electroluxair/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/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.electroluxair/README.md b/bundles/org.openhab.binding.electroluxair/README.md
new file mode 100644
index 000000000..c7637a407
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/README.md
@@ -0,0 +1,92 @@
+# ElectroluxAir Binding
+
+This is an openHAB binding for the Pure A9 Air Purifier, by Electrolux.
+
+This binding uses the Electrolux Delta REST API.
+
+![Electrolux Pure A9](doc/electrolux_pure_a9.png)
+
+## Supported Things
+
+This binding supports the following thing types:
+
+- api: Bridge - Implements the API that is used to communicate with the Air Purifier
+
+
+- electroluxpurea9: The Pure A9 Air Purifier
+
+## Discovery
+
+After the configuration of the Bridge, your Electrolux Pure A9 device will be automatically discovered and placed as a thing in the inbox.
+
+
+### Configuration Options
+
+Only the bridge require manual configuration. The Electrolux Pure A9 thing can be added by hand, or you can let the discovery mechanism automatically find it.
+
+
+#### Bridge
+
+| Parameter | Description | Type | Default | Required |
+|-----------|--------------------------------------------------------------|--------|----------|----------|
+| username | The username used to connect to the Electrolux Wellbeing app | String | NA | yes |
+| password | The password used to connect to the Electrolux Wellbeing app | String | NA | yes |
+| refresh | Specifies the refresh interval in second | Number | 600 | yes |
+
+#### Electrolux Pure A9
+
+| Parameter | Description | Type | Default | Required |
+|-----------|-------------------------------------------------------------------------|--------|----------|----------|
+| deviceId | Product ID of your Electrolux Pure A9 found in Electrolux Wellbeing app | Number | NA | yes |
+
+
+## Channels
+
+### Electrolux Pure A9
+
+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:Density | This channel reports the total Volatile Organic Compounds in microgram/m3. |
+| pm1 | Number:Dimensionless | This channel reports the Particulate Matter 1 in ppb. |
+| pm2_5 | Number:Dimensionless | This channel reports the Particulate Matter 2.5 in ppb. |
+| pm10 | Number:Dimensionless | This channel reports the Particulate Matter 10 in ppb. |
+| co2 | Number:Dimensionless | This channel reports the CO2 level in ppm. |
+| fanSpeed | Number | This channel sets and reports the current fan speed (1-9). |
+| filterLife | 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). |
+| doorOpen | Contact | This channel reports the status of door (Opened/Closed). |
+| workMode | String | This channel sets and reports the current work mode (Auto, Manual, PowerOff.)|
+
+
+## Full Example
+
+### Things-file
+
+````
+// Bridge configuration
+Bridge electroluxair:api:myAPI "Electrolux Delta API" [username="user@password.com", password="12345", refresh="300"] {
+
+ Thing electroluxpurea9 myElectroluxPureA9 "Electrolux Pure A9" [ deviceId="123456789" ]
+
+}
+````
+
+## Items-file
+
+````
+// CO2
+Number ElectroluxAirCO2 "Electrolux Air CO2 [%d ppm]" {channel="electroluxair:electroluxpurea9:myAPI:MyElectroluxPureA9:co2"}
+// Temperature
+Number:Temperature ElectroluxAirTemperature "Electrolux Air Temperature" {channel="electroluxair:electroluxpurea9:myAPI:myElectroluxPureA9:temperature"}
+// Door status
+Contact ElectroluxAirDoor "Electrolux Air Door Status" {channel="electroluxair:electroluxpurea9:myAPI:myElectroluxPureA9:doorOpen"}
+// Work mode
+String ElectroluxAirWorkModeSetting "ElectroluxAir Work Mode Setting" {channel="electroluxair:electroluxpurea9:myAPI:myElectroluxPureA9:workMode"}
+// Fan speed
+Number ElectroluxAirFanSpeed "Electrolux Air Fan Speed Setting" {channel="electroluxair:electroluxpurea9:myAPI:myElectroluxPureA9:fanSpeed"}
+````
+
diff --git a/bundles/org.openhab.binding.electroluxair/doc/electrolux_pure_a9.png b/bundles/org.openhab.binding.electroluxair/doc/electrolux_pure_a9.png
new file mode 100644
index 000000000..430ca014c
Binary files /dev/null and b/bundles/org.openhab.binding.electroluxair/doc/electrolux_pure_a9.png differ
diff --git a/bundles/org.openhab.binding.electroluxair/pom.xml b/bundles/org.openhab.binding.electroluxair/pom.xml
new file mode 100644
index 000000000..9b295fbfa
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.3.0-SNAPSHOT
+
+
+ org.openhab.binding.electroluxair
+
+ openHAB Add-ons :: Bundles :: ElectroluxAir Binding
+
+
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/feature/feature.xml b/bundles/org.openhab.binding.electroluxair/src/main/feature/feature.xml
new file mode 100644
index 000000000..3ac1bc7c9
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/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.electroluxair/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBindingConstants.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBindingConstants.java
new file mode 100644
index 000000000..5cce00807
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBindingConstants.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link ElectroluxAirBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxAirBindingConstants {
+
+ public static final String BINDING_ID = "electroluxair";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_ELECTROLUX_PURE_A9 = new ThingTypeUID(BINDING_ID, "electroluxpurea9");
+ public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "api");
+
+ // List of all Channel ids
+ 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 = "filterLife";
+ public static final String CHANNEL_DOOR_OPEN = "doorOpen";
+ public static final String CHANNEL_FAN_SPEED = "fanSpeed";
+ public static final String CHANNEL_WORK_MODE = "workMode";
+ public static final String CHANNEL_IONIZER = "ionizer";
+
+ // 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
+ 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_PURE_A9);
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBridgeConfiguration.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBridgeConfiguration.java
new file mode 100644
index 000000000..afc271522
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBridgeConfiguration.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link ElectroluxAirBridgeConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxAirBridgeConfiguration {
+ public @Nullable String username;
+ public @Nullable String password;
+ public int refresh;
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirConfiguration.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirConfiguration.java
new file mode 100644
index 000000000..28a974af8
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirConfiguration.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ElectroluxAirConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxAirConfiguration {
+ public static final String DEVICE_ID_LABEL = "deviceId";
+
+ private String deviceId = "";
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirException.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirException.java
new file mode 100644
index 000000000..b67211d67
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirException.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link ElectroluxAirException} is used when there is exception communicating with Electrolux Delta API.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxAirException extends Exception {
+
+ private static final long serialVersionUID = 2543564118231301159L;
+
+ public ElectroluxAirException(Exception source) {
+ super(source);
+ }
+
+ public ElectroluxAirException(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.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/api/ElectroluxDeltaAPI.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/api/ElectroluxDeltaAPI.java
new file mode 100644
index 000000000..a2b0d85a7
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/api/ElectroluxDeltaAPI.java
@@ -0,0 +1,314 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal.api;
+
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+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.electroluxair.internal.ElectroluxAirBridgeConfiguration;
+import org.openhab.binding.electroluxair.internal.ElectroluxAirException;
+import org.openhab.binding.electroluxair.internal.dto.ElectroluxPureA9DTO;
+import org.openhab.binding.electroluxair.internal.dto.ElectroluxPureA9DTO.AppliancesInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link ElectroluxDeltaAPI} class defines the Elextrolux Delta API
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxDeltaAPI {
+ private static final String CLIENT_URL = "https://electrolux-wellbeing-client.vercel.app/api/mu52m5PR9X";
+ private static final String SERVICE_URL = "https://api.delta.electrolux.com/api/";
+ private static final String JSON_CONTENT_TYPE = "application/json";
+ private static final String LOGIN = "Users/Login";
+ private static final int MAX_RETRIES = 3;
+
+ private final Logger logger = LoggerFactory.getLogger(ElectroluxDeltaAPI.class);
+ private final Gson gson;
+ private final HttpClient httpClient;
+ private final ElectroluxAirBridgeConfiguration configuration;
+ private String authToken = "";
+
+ public ElectroluxDeltaAPI(ElectroluxAirBridgeConfiguration configuration, Gson gson, HttpClient httpClient) {
+ this.gson = gson;
+ this.configuration = configuration;
+ this.httpClient = httpClient;
+ }
+
+ public boolean refresh(Map electroluxAirThings) {
+ try {
+ // Login
+ login();
+ // Get all appliances
+ String json = getAppliances();
+ JsonArray jsonArray = JsonParser.parseString(json).getAsJsonArray();
+
+ for (JsonElement jsonElement : jsonArray) {
+ String pncId = jsonElement.getAsJsonObject().get("pncId").getAsString();
+
+ // Get appliance info
+ String jsonApplianceInfo = getAppliancesInfo(pncId);
+ AppliancesInfo appliancesInfo = gson.fromJson(jsonApplianceInfo, AppliancesInfo.class);
+
+ // Get applicance data
+ ElectroluxPureA9DTO dto = getAppliancesData(pncId, ElectroluxPureA9DTO.class);
+ if (appliancesInfo != null) {
+ dto.setApplicancesInfo(appliancesInfo);
+ }
+ electroluxAirThings.put(dto.getTwin().getProperties().getReported().deviceId, dto);
+ }
+ return true;
+ } catch (ElectroluxAirException e) {
+ logger.warn("Failed to refresh! {}", e.getMessage());
+ }
+ return false;
+ }
+
+ public boolean workModePowerOff(String pncId) {
+ String commandJSON = "{ \"WorkMode\": \"PowerOff\" }";
+ try {
+ return sendCommand(commandJSON, pncId);
+ } catch (ElectroluxAirException e) {
+ logger.warn("Work mode powerOff failed {}", e.getMessage());
+ }
+ return false;
+ }
+
+ public boolean workModeAuto(String pncId) {
+ String commandJSON = "{ \"WorkMode\": \"Auto\" }";
+ try {
+ return sendCommand(commandJSON, pncId);
+ } catch (ElectroluxAirException e) {
+ logger.warn("Work mode auto failed {}", e.getMessage());
+ }
+ return false;
+ }
+
+ public boolean workModeManual(String pncId) {
+ String commandJSON = "{ \"WorkMode\": \"Manual\" }";
+ try {
+ return sendCommand(commandJSON, pncId);
+ } catch (ElectroluxAirException e) {
+ logger.warn("Work mode manual failed {}", e.getMessage());
+ }
+ return false;
+ }
+
+ public boolean setFanSpeedLevel(String pncId, int fanSpeedLevel) {
+ if (fanSpeedLevel < 1 && fanSpeedLevel > 10) {
+ return false;
+ } else {
+ String commandJSON = "{ \"Fanspeed\": " + fanSpeedLevel + "}";
+ try {
+ return sendCommand(commandJSON, pncId);
+ } catch (ElectroluxAirException e) {
+ logger.warn("Work mode manual failed {}", e.getMessage());
+ }
+ }
+ return false;
+ }
+
+ public boolean setIonizer(String pncId, String ionizerStatus) {
+ String commandJSON = "{ \"Ionizer\": " + ionizerStatus + "}";
+ try {
+ return sendCommand(commandJSON, pncId);
+ } catch (ElectroluxAirException e) {
+ logger.warn("Work mode manual failed {}", e.getMessage());
+ }
+ return false;
+ }
+
+ private void login() throws ElectroluxAirException {
+ // Fetch ClientToken
+ Request request = httpClient.newRequest(CLIENT_URL).method(HttpMethod.GET);
+
+ request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
+ request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
+
+ logger.debug("HTTP GET Request {}.", request.toString());
+ try {
+ ContentResponse httpResponse = request.send();
+ if (httpResponse.getStatus() != HttpStatus.OK_200) {
+ throw new ElectroluxAirException("Failed to login " + httpResponse.getContentAsString());
+ }
+ String json = httpResponse.getContentAsString();
+ JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
+ String clientToken = jsonObject.get("accessToken").getAsString();
+
+ // Login using ClientToken
+ json = "{ \"Username\": \"" + configuration.username + "\", \"Password\": \"" + configuration.password
+ + "\" }";
+ request = httpClient.newRequest(SERVICE_URL + LOGIN).method(HttpMethod.POST);
+ request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
+ request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
+ request.header(HttpHeader.AUTHORIZATION, "Bearer " + clientToken);
+ request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
+
+ logger.debug("HTTP POST Request {}.", request.toString());
+
+ httpResponse = request.send();
+ if (httpResponse.getStatus() != HttpStatus.OK_200) {
+ throw new ElectroluxAirException("Failed to login " + httpResponse.getContentAsString());
+ }
+ // Fetch AccessToken
+ json = httpResponse.getContentAsString();
+ jsonObject = JsonParser.parseString(json).getAsJsonObject();
+ this.authToken = jsonObject.get("accessToken").getAsString();
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ throw new ElectroluxAirException(e);
+ }
+ }
+
+ private String getFromApi(String uri) throws ElectroluxAirException, InterruptedException {
+ try {
+ for (int i = 0; i < MAX_RETRIES; i++) {
+ try {
+ Request request = httpClient.newRequest(SERVICE_URL + uri).method(HttpMethod.GET);
+ request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
+ request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
+ request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
+
+ 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());
+ login();
+ } else {
+ return content;
+ }
+ } catch (TimeoutException e) {
+ logger.debug("TimeoutException error in get: {}", e.getMessage());
+ }
+ }
+ throw new ElectroluxAirException("Failed to fetch from API!");
+ } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
+ throw new ElectroluxAirException(e);
+ }
+ }
+
+ private String getAppliances() throws ElectroluxAirException {
+ String uri = "Domains/Appliances";
+ try {
+ return getFromApi(uri);
+ } catch (ElectroluxAirException | InterruptedException e) {
+ throw new ElectroluxAirException(e);
+ }
+ }
+
+ private String getAppliancesInfo(String pncId) throws ElectroluxAirException {
+ String uri = "AppliancesInfo/" + pncId;
+ try {
+ return getFromApi(uri);
+ } catch (ElectroluxAirException | InterruptedException e) {
+ throw new ElectroluxAirException(e);
+ }
+ }
+
+ private T getAppliancesData(String pncId, Class dto) throws ElectroluxAirException {
+ String uri = "Appliances/" + pncId;
+ String json;
+
+ try {
+ json = getFromApi(uri);
+ } catch (ElectroluxAirException | InterruptedException e) {
+ throw new ElectroluxAirException(e);
+ }
+ return gson.fromJson(json, dto);
+ }
+
+ private boolean sendCommand(String commandJSON, String pncId) throws ElectroluxAirException {
+ String uri = "Appliances/" + pncId + "/Commands";
+ try {
+ for (int i = 0; i < MAX_RETRIES; i++) {
+ try {
+ Request request = httpClient.newRequest(SERVICE_URL + uri).method(HttpMethod.PUT);
+ request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
+ request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
+ request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
+ request.content(new StringContentProvider(commandJSON), JSON_CONTENT_TYPE);
+
+ 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());
+ login();
+ } else {
+ CommandResponseDTO commandResponse = gson.fromJson(content, CommandResponseDTO.class);
+ if (commandResponse != null) {
+ if (commandResponse.code == 200000) {
+ return true;
+ } else {
+ logger.warn("Failed to send command, error code: {}, description: {}",
+ commandResponse.code, commandResponse.codeDescription);
+ return false;
+ }
+ } else {
+ logger.warn("Failed to send command, commandResponse is null!");
+ return false;
+ }
+ }
+ } catch (TimeoutException | InterruptedException e) {
+ logger.warn("TimeoutException error in get");
+ }
+ }
+ } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
+ throw new ElectroluxAirException(e);
+ }
+ return false;
+ }
+
+ @SuppressWarnings("unused")
+ private static class CommandResponseDTO {
+ public int code;
+ public String codeDescription = "";
+ public String information = "";
+ public String message = "";
+ public PayloadDTO payload = new PayloadDTO();
+ public int status;
+ }
+
+ private static class PayloadDTO {
+ @SerializedName("Ok")
+ public boolean ok;
+ @SerializedName("Response")
+ public ResponseDTO response = new ResponseDTO();
+ }
+
+ private static class ResponseDTO {
+ @SerializedName("Workmode")
+ public String workmode = "";
+ }
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/discovery/ElectroluxAirDiscoveryService.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/discovery/ElectroluxAirDiscoveryService.java
new file mode 100644
index 000000000..16138460a
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/discovery/ElectroluxAirDiscoveryService.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal.discovery;
+
+import static org.openhab.binding.electroluxair.internal.ElectroluxAirBindingConstants.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.electroluxair.internal.ElectroluxAirConfiguration;
+import org.openhab.binding.electroluxair.internal.handler.ElectroluxAirBridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+
+/**
+ * The {@link ElectroluxAirDiscoveryService} searches for available
+ * Electrolux Pure A9 discoverable through Electrolux Delta API.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxAirDiscoveryService extends AbstractDiscoveryService
+ implements ThingHandlerService, DiscoveryService {
+ private static final int SEARCH_TIME = 2;
+ private @Nullable ElectroluxAirBridgeHandler handler;
+
+ public ElectroluxAirDiscoveryService() {
+ super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof ElectroluxAirBridgeHandler) {
+ this.handler = (ElectroluxAirBridgeHandler) handler;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler;
+ }
+
+ @Override
+ public void activate(@Nullable Map configProperties) {
+ super.activate(configProperties);
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ protected void startScan() {
+ ElectroluxAirBridgeHandler bridgeHandler = this.handler;
+ if (bridgeHandler != null) {
+ ThingUID bridgeUID = bridgeHandler.getThing().getUID();
+ bridgeHandler.getElectroluxAirThings().entrySet().stream().forEach(thing -> {
+ thingDiscovered(DiscoveryResultBuilder
+ .create(new ThingUID(THING_TYPE_ELECTROLUX_PURE_A9, bridgeUID, thing.getKey()))
+ .withLabel("Electrolux Pure A9").withBridge(bridgeUID)
+ .withProperty(ElectroluxAirConfiguration.DEVICE_ID_LABEL, thing.getKey())
+ .withRepresentationProperty(ElectroluxAirConfiguration.DEVICE_ID_LABEL).build());
+ });
+ }
+ stopScan();
+ }
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/dto/ElectroluxPureA9DTO.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/dto/ElectroluxPureA9DTO.java
new file mode 100644
index 000000000..d05751da7
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/dto/ElectroluxPureA9DTO.java
@@ -0,0 +1,581 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link ElectroluxPureA9DTO} class defines the DTO for the Electrolux Pure A9.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxPureA9DTO {
+ public String pncId = "";
+ public ApplianceData applianceData = new ApplianceData();
+ public AppliancesInfo applicancesInfo = new AppliancesInfo();
+
+ public Twin twin = new Twin();
+ public String telemetry = "";
+
+ public String getPncId() {
+ return pncId;
+ }
+
+ public ApplianceData getApplianceData() {
+ return applianceData;
+ }
+
+ public AppliancesInfo getApplicancesInfo() {
+ return applicancesInfo;
+ }
+
+ public void setApplicancesInfo(AppliancesInfo applicancesInfo) {
+ this.applicancesInfo = applicancesInfo;
+ }
+
+ public Twin getTwin() {
+ return twin;
+ }
+
+ public String getTelemetry() {
+ return telemetry;
+ }
+
+ public class MetaData1 {
+
+ @SerializedName("$lastUpdated")
+ public String lastUpdated1 = "";
+ @SerializedName("$lastUpdatedVersion")
+ public int lastUpdatedVersion1;
+ @SerializedName("TimeZoneStandardName")
+ public TimeZoneStandardName timeZoneStandardName = new TimeZoneStandardName();
+ @SerializedName("FrmVer_NIU")
+ public FrmVerNIU frmVerNIU = new FrmVerNIU();
+ }
+
+ public class Metadata2 {
+
+ @SerializedName("$lastUpdated")
+ public String lastUpdated2 = "";
+ @SerializedName("FrmVer_NIU")
+ public FrmVerNIU frmVerNIU = new FrmVerNIU();
+ @SerializedName("Workmode")
+ public Workmode workmode = new Workmode();
+ @SerializedName("FilterRFID")
+ public FilterRFID filterRFID = new FilterRFID();
+ @SerializedName("FilterLife")
+ public FilterLife filterLife = new FilterLife();
+ @SerializedName("Fanspeed")
+ public Fanspeed fanspeed = new Fanspeed();
+ @SerializedName("UILight")
+ public UILight uILight = new UILight();
+ @SerializedName("SafetyLock")
+ public SafetyLock safetyLock = new SafetyLock();
+ @SerializedName("Ionizer")
+ public Ionizer ionizer = new Ionizer();
+ @SerializedName("Sleep")
+ public Sleep sleep = new Sleep();
+ @SerializedName("Scheduler")
+ public Scheduler scheduler = new Scheduler();
+ @SerializedName("FilterType")
+ public FilterType filterType = new FilterType();
+ @SerializedName("DspIcoPM2_5")
+ public DspIcoPM25 dspIcoPM25 = new DspIcoPM25();
+ @SerializedName("DspIcoPM1")
+ public DspIcoPM1 dspIcoPM1 = new DspIcoPM1();
+ @SerializedName("DspIcoPM10")
+ public DspIcoPM10 dspIcoPM10 = new DspIcoPM10();
+ @SerializedName("DspIcoTVOC")
+ public DspIcoTVOC dspIcoTVOC = new DspIcoTVOC();
+ @SerializedName("ErrPM2_5")
+ public ErrPM25 errPM25 = new ErrPM25();
+ @SerializedName("ErrTVOC")
+ public ErrTVOC errTVOC = new ErrTVOC();
+ @SerializedName("ErrTempHumidity")
+ public ErrTempHumidity errTempHumidity = new ErrTempHumidity();
+ @SerializedName("ErrFanMtr")
+ public ErrFanMtr errFanMtr = new ErrFanMtr();
+ @SerializedName("ErrCommSensorDisplayBrd")
+ public ErrCommSensorDisplayBrd errCommSensorDisplayBrd = new ErrCommSensorDisplayBrd();
+ @SerializedName("DoorOpen")
+ public DoorOpen doorOpen = new DoorOpen();
+ @SerializedName("ErrRFID")
+ public ErrRFID errRFID = new ErrRFID();
+ @SerializedName("SignalStrength")
+ public SignalStrength signalStrength = new SignalStrength();
+ @SerializedName("PM1")
+ public PM1 pM1 = new PM1();
+ @SerializedName("PM2_5")
+ public PM25 pM25 = new PM25();
+ @SerializedName("PM10")
+ public PM10 pM10 = new PM10();
+ @SerializedName("TVOC")
+ public TVOC tVOC = new TVOC();
+ @SerializedName("CO2")
+ public CO2 cO2 = new CO2();
+ @SerializedName("Temp")
+ public Temp temp = new Temp();
+ @SerializedName("Humidity")
+ public Humidity humidity = new Humidity();
+ @SerializedName("EnvLightLvl")
+ public EnvLightLvl envLightLvl = new EnvLightLvl();
+ @SerializedName("RSSI")
+ public RSSI rSSI = new RSSI();
+ }
+
+ public class ApplianceData {
+
+ public String applianceName = "";
+ public String created = "";
+ public String modelName = "";
+ public String pncId = "";
+ }
+
+ public class AppliancesInfo {
+ public String brand = "";
+ public String colour = "";
+ public String device = "";
+ public String model = "";
+ public String serialNumber = "";
+ }
+
+ public class CO2 {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated3 = "";
+ }
+
+ public class Desired {
+
+ @SerializedName("TimeZoneStandardName")
+ public String timeZoneStandardName = "";
+ @SerializedName("FrmVer_NIU")
+ public String frmVerNIU = "";
+ @SerializedName("$metadata")
+ public MetaData1 metadata3 = new MetaData1();
+ @SerializedName("$version")
+ public int version;
+ }
+
+ public class DoorOpen {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class DspIcoPM1 {
+ @SerializedName("lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class DspIcoPM10 {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class DspIcoPM25 {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class DspIcoTVOC {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class EnvLightLvl {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class ErrCommSensorDisplayBrd {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class ErrFanMtr {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class ErrPM25 {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class ErrRFID {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class ErrTVOC {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class ErrTempHumidity {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Fanspeed {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class FilterLife {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class FilterRFID {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class FilterType {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class FrmVerNIU {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ @SerializedName("$lastUpdatedVersion")
+ public int lastUpdatedVersion;
+ }
+
+ // public class FrmVerNIU_ {
+ // @SerializedName("$lastUpdated")
+ // public String lastUpdated = "";
+ // }
+
+ public class Humidity {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Ionizer {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class PM1 {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class PM10 {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class PM25 {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Properties {
+ public Desired desired = new Desired();
+ public Reported reported = new Reported();
+
+ public Reported getReported() {
+ return reported;
+ }
+ }
+
+ public class RSSI {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Reported {
+
+ @SerializedName("FrmVer_NIU")
+ public String frmVerNIU = "";
+ @SerializedName("Workmode")
+ public String workmode = "";
+ @SerializedName("FilterRFID")
+ public String filterRFID = "";
+ @SerializedName("FilterLife")
+ public int filterLife;
+ @SerializedName("Fanspeed")
+ public int fanspeed;
+ @SerializedName("UILight")
+ public boolean uILight;
+ @SerializedName("SafetyLock")
+ public boolean safetyLock;
+ @SerializedName("Ionizer")
+ public boolean ionizer;
+ @SerializedName("Sleep")
+ public boolean sleep;
+ @SerializedName("Scheduler")
+ public boolean scheduler;
+ @SerializedName("FilterType")
+ public int filterType;
+ @SerializedName("DspIcoPM2_5")
+ public boolean dspIcoPM25;
+ @SerializedName("DspIcoPM1")
+ public boolean dspIcoPM1;
+ @SerializedName("DspIcoPM10")
+ public boolean dspIcoPM10;
+ @SerializedName("DspIcoTVOC")
+ public boolean dspIcoTVOC;
+ @SerializedName("ErrPM2_5")
+ public boolean errPM25;
+ @SerializedName("ErrTVOC")
+ public boolean errTVOC;
+ @SerializedName("ErrTempHumidity")
+ public boolean errTempHumidity;
+ @SerializedName("ErrFanMtr")
+ public boolean errFanMtr;
+ @SerializedName("ErrCommSensorDisplayBrd")
+ public boolean errCommSensorDisplayBrd;
+ @SerializedName("DoorOpen")
+ public boolean doorOpen;
+ @SerializedName("ErrRFID")
+ public boolean errRFID;
+ @SerializedName("SignalStrength")
+ public String signalStrength = "";
+ @SerializedName("$metadata")
+ public Metadata2 metadata2 = new Metadata2();
+ @SerializedName("$version")
+ public int version;
+ public String deviceId = "";
+ @SerializedName("PM1")
+ public int pM1;
+ @SerializedName("PM2_5")
+ public int pM25;
+ @SerializedName("PM10")
+ public int pM10;
+ @SerializedName("TVOC")
+ public int tVOC;
+ @SerializedName("CO2")
+ public int cO2;
+ @SerializedName("Temp")
+ public int temp;
+ @SerializedName("Humidity")
+ public int humidity;
+ @SerializedName("EnvLightLvl")
+ public int envLightLvl;
+ @SerializedName("RSSI")
+ public int rSSI;
+
+ 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 getVersion() {
+ return version;
+ }
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public int getpM1() {
+ return pM1;
+ }
+
+ public int getpM25() {
+ return pM25;
+ }
+
+ public int getpM10() {
+ return pM10;
+ }
+
+ public int gettVOC() {
+ return tVOC;
+ }
+
+ public int getcO2() {
+ return cO2;
+ }
+
+ public int getTemp() {
+ return temp;
+ }
+
+ public int getHumidity() {
+ return humidity;
+ }
+
+ public int getEnvLightLvl() {
+ return envLightLvl;
+ }
+
+ public int getrSSI() {
+ return rSSI;
+ }
+ }
+
+ public class SafetyLock {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Scheduler {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class SignalStrength {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Sleep {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class TVOC {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Temp {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class TimeZoneStandardName {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ @SerializedName("$lastUpdatedVersion")
+ public int lastUpdatedVersion;
+ }
+
+ public class Twin {
+ public String deviceId = "";
+ public Properties properties = new Properties();
+ public String status = "";
+ public String connectionState = "";
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public Properties getProperties() {
+ return properties;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getConnectionState() {
+ return connectionState;
+ }
+ }
+
+ public class UILight {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+
+ public class Workmode {
+ @SerializedName("$lastUpdated")
+ public String lastUpdated = "";
+ }
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirBridgeHandler.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirBridgeHandler.java
new file mode 100644
index 000000000..0edf43206
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirBridgeHandler.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal.handler;
+
+import static org.openhab.binding.electroluxair.internal.ElectroluxAirBindingConstants.THING_TYPE_BRIDGE;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+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.electroluxair.internal.ElectroluxAirBridgeConfiguration;
+import org.openhab.binding.electroluxair.internal.api.ElectroluxDeltaAPI;
+import org.openhab.binding.electroluxair.internal.discovery.ElectroluxAirDiscoveryService;
+import org.openhab.binding.electroluxair.internal.dto.ElectroluxPureA9DTO;
+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 com.google.gson.Gson;
+
+/**
+ * The {@link ElectroluxAirBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxAirBridgeHandler extends BaseBridgeHandler {
+
+ public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
+
+ private int refreshTimeInSeconds = 300;
+
+ private final Gson gson;
+ private final HttpClient httpClient;
+ private final Map electroluxAirThings = new ConcurrentHashMap<>();
+
+ private @Nullable ElectroluxDeltaAPI api;
+ private @Nullable ScheduledFuture> refreshJob;
+
+ public ElectroluxAirBridgeHandler(Bridge bridge, HttpClient httpClient, Gson gson) {
+ super(bridge);
+ this.httpClient = httpClient;
+ this.gson = gson;
+ }
+
+ @Override
+ public void initialize() {
+ ElectroluxAirBridgeConfiguration config = getConfigAs(ElectroluxAirBridgeConfiguration.class);
+
+ ElectroluxDeltaAPI electroluxDeltaAPI = new ElectroluxDeltaAPI(config, gson, httpClient);
+ refreshTimeInSeconds = config.refresh;
+
+ if (config.username == null || config.password == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Configuration of username, password is mandatory");
+ } else if (refreshTimeInSeconds < 0) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Refresh time cannot be negative!");
+ } else {
+ try {
+ this.api = electroluxDeltaAPI;
+ scheduler.execute(() -> {
+ updateStatus(ThingStatus.UNKNOWN);
+ startAutomaticRefresh();
+
+ });
+ } catch (RuntimeException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+ }
+
+ public Map getElectroluxAirThings() {
+ return electroluxAirThings;
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Collections.singleton(ElectroluxAirDiscoveryService.class);
+ }
+
+ @Override
+ public void dispose() {
+ stopAutomaticRefresh();
+ }
+
+ public @Nullable ElectroluxDeltaAPI getElectroluxDeltaAPI() {
+ return api;
+ }
+
+ private boolean refreshAndUpdateStatus() {
+ if (api != null) {
+ if (api.refresh(electroluxAirThings)) {
+ getThing().getThings().stream().forEach(thing -> {
+ ElectroluxAirHandler handler = (ElectroluxAirHandler) thing.getHandler();
+ if (handler != null) {
+ handler.update();
+ }
+ });
+ updateStatus(ThingStatus.ONLINE);
+ return true;
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+ }
+ 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;
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ return;
+ }
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandler.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandler.java
new file mode 100644
index 000000000..0b2f37717
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandler.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal.handler;
+
+import static org.openhab.binding.electroluxair.internal.ElectroluxAirBindingConstants.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.electroluxair.internal.ElectroluxAirConfiguration;
+import org.openhab.binding.electroluxair.internal.api.ElectroluxDeltaAPI;
+import org.openhab.binding.electroluxair.internal.dto.ElectroluxPureA9DTO;
+import org.openhab.core.library.dimension.Density;
+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.binding.BaseThingHandler;
+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 ElectroluxAirHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+public class ElectroluxAirHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(ElectroluxAirHandler.class);
+
+ private ElectroluxAirConfiguration config = new ElectroluxAirConfiguration();
+
+ public ElectroluxAirHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("Command received: {}", command);
+ if (CHANNEL_STATUS.equals(channelUID.getId()) || command instanceof RefreshType) {
+ update();
+ } else {
+ ElectroluxPureA9DTO dto = getElectroluxPureA9DTO();
+ ElectroluxDeltaAPI api = getElectroluxDeltaAPO();
+ if (api != null && dto != null) {
+ if (CHANNEL_WORK_MODE.equals(channelUID.getId())) {
+ if (command.toString().equals(COMMAND_WORKMODE_POWEROFF)) {
+ api.workModePowerOff(dto.getPncId());
+ } else if (command.toString().equals(COMMAND_WORKMODE_AUTO)) {
+ api.workModeAuto(dto.getPncId());
+ } else if (command.toString().equals(COMMAND_WORKMODE_MANUAL)) {
+ api.workModeManual(dto.getPncId());
+ }
+ } else if (CHANNEL_FAN_SPEED.equals(channelUID.getId())) {
+ api.setFanSpeedLevel(dto.getPncId(), Integer.parseInt(command.toString()));
+ } else if (CHANNEL_IONIZER.equals(channelUID.getId())) {
+ if (command == OnOffType.OFF) {
+ api.setIonizer(dto.getPncId(), "false");
+ } else if (command == OnOffType.ON) {
+ api.setIonizer(dto.getPncId(), "true");
+ } else {
+ logger.debug("Unknown command! {}", command);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(ElectroluxAirConfiguration.class);
+ updateStatus(ThingStatus.UNKNOWN);
+
+ scheduler.execute(() -> {
+ update();
+ Map properties = refreshProperties();
+ updateProperties(properties);
+ });
+ }
+
+ public void update() {
+ ElectroluxPureA9DTO dto = getElectroluxPureA9DTO();
+ if (dto != null) {
+ update(dto);
+ } else {
+ logger.warn("ElectroluxPureA9DTO is null!");
+ }
+ }
+
+ private @Nullable ElectroluxDeltaAPI getElectroluxDeltaAPO() {
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ ElectroluxAirBridgeHandler handler = (ElectroluxAirBridgeHandler) bridge.getHandler();
+ if (handler != null) {
+ return handler.getElectroluxDeltaAPI();
+ }
+ }
+ return null;
+ }
+
+ private @Nullable ElectroluxPureA9DTO getElectroluxPureA9DTO() {
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ ElectroluxAirBridgeHandler bridgeHandler = (ElectroluxAirBridgeHandler) bridge.getHandler();
+ if (bridgeHandler != null) {
+ return bridgeHandler.getElectroluxAirThings().get(config.getDeviceId());
+ }
+ }
+ return null;
+ }
+
+ private void update(@Nullable ElectroluxPureA9DTO 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);
+ updateState(channelUID, state);
+ });
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ private State getValue(String channelId, ElectroluxPureA9DTO dto) {
+ switch (channelId) {
+ case CHANNEL_TEMPERATURE:
+ return new QuantityType(dto.getTwin().getProperties().getReported().getTemp(),
+ SIUnits.CELSIUS);
+ case CHANNEL_HUMIDITY:
+ return new QuantityType(dto.getTwin().getProperties().getReported().getHumidity(),
+ Units.PERCENT);
+ case CHANNEL_TVOC:
+ return new QuantityType(dto.getTwin().getProperties().getReported().gettVOC(),
+ Units.MICROGRAM_PER_CUBICMETRE);
+ case CHANNEL_PM1:
+ return new QuantityType(dto.getTwin().getProperties().getReported().getpM1(),
+ Units.PARTS_PER_BILLION);
+ case CHANNEL_PM25:
+ return new QuantityType(dto.getTwin().getProperties().getReported().getpM25(),
+ Units.PARTS_PER_BILLION);
+ case CHANNEL_PM10:
+ return new QuantityType(dto.getTwin().getProperties().getReported().getpM10(),
+ Units.PARTS_PER_BILLION);
+ case CHANNEL_CO2:
+ return new QuantityType(dto.getTwin().getProperties().getReported().getcO2(),
+ Units.PARTS_PER_MILLION);
+ case CHANNEL_FAN_SPEED:
+ return new StringType(Integer.toString(dto.getTwin().getProperties().getReported().getFanspeed()));
+ case CHANNEL_FILTER_LIFE:
+ return new QuantityType(dto.getTwin().getProperties().getReported().getFilterLife(),
+ Units.PERCENT);
+ case CHANNEL_IONIZER:
+ return OnOffType.from(dto.getTwin().getProperties().getReported().ionizer);
+ case CHANNEL_WORK_MODE:
+ return new StringType(dto.getTwin().getProperties().getReported().workmode);
+ case CHANNEL_DOOR_OPEN:
+ return dto.getTwin().getProperties().getReported().doorOpen ? OpenClosedType.OPEN
+ : OpenClosedType.CLOSED;
+ }
+ return UnDefType.UNDEF;
+ }
+
+ private Map refreshProperties() {
+ Map properties = new HashMap<>();
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ ElectroluxAirBridgeHandler bridgeHandler = (ElectroluxAirBridgeHandler) bridge.getHandler();
+ if (bridgeHandler != null) {
+ ElectroluxPureA9DTO dto = bridgeHandler.getElectroluxAirThings().get(config.getDeviceId());
+ if (dto != null) {
+ properties.put(Thing.PROPERTY_VENDOR, dto.getApplicancesInfo().brand);
+ properties.put(PROPERTY_COLOUR, dto.getApplicancesInfo().colour);
+ properties.put(PROPERTY_DEVICE, dto.getApplicancesInfo().device);
+ properties.put(Thing.PROPERTY_MODEL_ID, dto.getApplicancesInfo().model);
+ properties.put(Thing.PROPERTY_SERIAL_NUMBER, dto.getApplicancesInfo().serialNumber);
+ properties.put(Thing.PROPERTY_FIRMWARE_VERSION,
+ dto.getTwin().getProperties().getReported().frmVerNIU);
+ }
+ }
+ }
+ return properties;
+ }
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandlerFactory.java b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandlerFactory.java
new file mode 100644
index 000000000..6d7416b4d
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandlerFactory.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2022 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.electroluxair.internal.handler;
+
+import static org.openhab.binding.electroluxair.internal.ElectroluxAirBindingConstants.*;
+
+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 com.google.gson.Gson;
+
+/**
+ * The {@link ElectroluxAirHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Jan Gustafsson - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.electroluxair", service = ThingHandlerFactory.class)
+public class ElectroluxAirHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ELECTROLUX_PURE_A9,
+ THING_TYPE_BRIDGE);
+ private final Gson gson;
+ private final HttpClient httpClient;
+
+ @Activate
+ public ElectroluxAirHandlerFactory(@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_PURE_A9.equals(thingTypeUID)) {
+ return new ElectroluxAirHandler(thing);
+ } else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
+ return new ElectroluxAirBridgeHandler((Bridge) thing, httpClient, gson);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 000000000..29cc21fe8
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ ElectroluxAir Binding
+ This is the binding for Electrolux Pure A9 Air Purifier.
+
+
diff --git a/bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 000000000..a69ca5750
--- /dev/null
+++ b/bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,185 @@
+
+
+
+
+
+ This bridge represents the web API connector.
+
+
+ Electrolux
+
+
+
+
+
+ The username used to login to Electrolux Wellbeing app.
+
+
+
+ password
+ The password used to login to Electrolux Wellbeing app.
+
+
+
+ Specifies the refresh interval in seconds.
+ 300
+
+
+
+
+
+
+
+
+
+
+ This thing represents the ElectroluxAir Pure A9.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Electrolux
+
+
+ deviceId
+
+
+
+
+ Unique Id.
+
+
+
+
+
+
+ String
+
+ Information on current status.
+
+
+
+
+ Number:Temperature
+
+ Temperature
+ Temperature
+
+
+
+
+
+ Number:Dimensionless
+
+ Humidity
+ Humidity
+
+
+
+
+ Number:Density
+
+ Total Volatile Organic Compounds
+
+
+
+
+
+ Number:Dimensionless
+
+ Particulate Matter 1 (0.001mm)
+
+
+
+
+ Number:Dimensionless
+
+ Particulate Matter 2.5 (0.0025mm)
+
+
+
+
+ Number:Dimensionless
+
+ Particulate Matter 10 (0.01mm)
+
+
+
+
+ Number:Dimensionless
+
+ CarbonDioxide
+
+
+
+
+ Number:Dimensionless
+
+ Filter Life
+
+
+
+
+ Contact
+
+ Door Status Open/Closed
+
+
+
+
+ Number
+
+ Fan Speed Setting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ Work Mode Setting
+
+
+
+
+
+
+
+
+
+
+ Switch
+
+ Ionizer Status
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index e3469531f..536eef333 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -112,6 +112,7 @@
org.openhab.binding.ecobee
org.openhab.binding.ecotouch
org.openhab.binding.ekey
+ org.openhab.binding.electroluxair
org.openhab.binding.elerotransmitterstick
org.openhab.binding.energenie
org.openhab.binding.enigma2