]> git.basschouten.com Git - openhab-addons.git/commitdiff
[electroluxair] Initial contribution (#11116)
authorJan Gustafsson <jannegpriv@gmail.com>
Sun, 9 Jan 2022 09:02:25 +0000 (10:02 +0100)
committerGitHub <noreply@github.com>
Sun, 9 Jan 2022 09:02:25 +0000 (10:02 +0100)
* [electroluxair] Initial contribution of the electroluxair openHAB binding

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
* Updated after code review.

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
* Updated after code review and also copyright to 2022.

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
* [electroluxair] Initial contribution of the electroluxair openHAB binding

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
* Updated after code review.

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
* Updated after code review

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
20 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.electroluxair/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/README.md [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/doc/electrolux_pure_a9.png [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/ElectroluxAirException.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/api/ElectroluxDeltaAPI.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/discovery/ElectroluxAirDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/dto/ElectroluxPureA9DTO.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/java/org/openhab/binding/electroluxair/internal/handler/ElectroluxAirHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.electroluxair/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 4be0fa8c60d70c3ffde6f6530bb517ec7951c16c..2393601417bccf5c4db369722f031f29eaf44b7c 100644 (file)
@@ -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
index caa29bac5ecee23b296b8774955efd54dfa8a924..4e06be806049933235e81937757bcd732ac2668f 100644 (file)
       <artifactId>org.openhab.binding.ekey</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.electroluxair</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.elerotransmitterstick</artifactId>
diff --git a/bundles/org.openhab.binding.electroluxair/NOTICE b/bundles/org.openhab.binding.electroluxair/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..c7637a4
--- /dev/null
@@ -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 (file)
index 0000000..430ca01
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 (file)
index 0000000..9b295fb
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.electroluxair</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: ElectroluxAir Binding</name>
+
+</project>
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 (file)
index 0000000..3ac1bc7
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.electroluxair-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-electroluxair" description="ElectroluxAir Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.electroluxair/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..5cce008
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..afc2715
--- /dev/null
@@ -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 (file)
index 0000000..28a974a
--- /dev/null
@@ -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 (file)
index 0000000..b67211d
--- /dev/null
@@ -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 (file)
index 0000000..a2b0d85
--- /dev/null
@@ -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<String, ElectroluxPureA9DTO> 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> T getAppliancesData(String pncId, Class<T> 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 (file)
index 0000000..1613846
--- /dev/null
@@ -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<String, Object> 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 (file)
index 0000000..d05751d
--- /dev/null
@@ -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 (file)
index 0000000..0edf432
--- /dev/null
@@ -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<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
+
+    private int refreshTimeInSeconds = 300;
+
+    private final Gson gson;
+    private final HttpClient httpClient;
+    private final Map<String, ElectroluxPureA9DTO> 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<String, ElectroluxPureA9DTO> getElectroluxAirThings() {
+        return electroluxAirThings;
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> 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 (file)
index 0000000..0b2f377
--- /dev/null
@@ -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<String, String> 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<Temperature>(dto.getTwin().getProperties().getReported().getTemp(),
+                        SIUnits.CELSIUS);
+            case CHANNEL_HUMIDITY:
+                return new QuantityType<Dimensionless>(dto.getTwin().getProperties().getReported().getHumidity(),
+                        Units.PERCENT);
+            case CHANNEL_TVOC:
+                return new QuantityType<Density>(dto.getTwin().getProperties().getReported().gettVOC(),
+                        Units.MICROGRAM_PER_CUBICMETRE);
+            case CHANNEL_PM1:
+                return new QuantityType<Dimensionless>(dto.getTwin().getProperties().getReported().getpM1(),
+                        Units.PARTS_PER_BILLION);
+            case CHANNEL_PM25:
+                return new QuantityType<Dimensionless>(dto.getTwin().getProperties().getReported().getpM25(),
+                        Units.PARTS_PER_BILLION);
+            case CHANNEL_PM10:
+                return new QuantityType<Dimensionless>(dto.getTwin().getProperties().getReported().getpM10(),
+                        Units.PARTS_PER_BILLION);
+            case CHANNEL_CO2:
+                return new QuantityType<Dimensionless>(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<Dimensionless>(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<String, String> refreshProperties() {
+        Map<String, String> 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 (file)
index 0000000..6d7416b
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..29cc21f
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="electroluxair" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>ElectroluxAir Binding</name>
+       <description>This is the binding for Electrolux Pure A9 Air Purifier.</description>
+
+</binding:binding>
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 (file)
index 0000000..a69ca57
--- /dev/null
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="electroluxair"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="api">
+               <label>Electrolux Delta API</label>
+               <description>This bridge represents the web API connector.</description>
+
+               <properties>
+                       <property name="vendor">Electrolux</property>
+               </properties>
+
+               <config-description>
+                       <parameter name="username" type="text" required="true">
+                               <label>Username</label>
+                               <description>The username used to login to Electrolux Wellbeing app.</description>
+                       </parameter>
+                       <parameter name="password" type="text" required="true">
+                               <label>Password</label>
+                               <context>password</context>
+                               <description>The password used to login to Electrolux Wellbeing app.</description>
+                       </parameter>
+                       <parameter name="refresh" type="integer" min="10" unit="s">
+                               <label>Refresh Interval</label>
+                               <description>Specifies the refresh interval in seconds.</description>
+                               <default>300</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="electroluxpurea9">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="api"/>
+               </supported-bridge-type-refs>
+
+               <label>ElectroluxAir Pure A9</label>
+               <description>This thing represents the ElectroluxAir Pure A9.</description>
+
+               <channels>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="humidity" typeId="humidity"/>
+                       <channel id="tvoc" typeId="tvoc"/>
+                       <channel id="pm1" typeId="pm1"/>
+                       <channel id="pm2_5" typeId="pm2_5"/>
+                       <channel id="pm10" typeId="pm10"/>
+                       <channel id="co2" typeId="co2"/>
+                       <channel id="filterLife" typeId="filterLife"/>
+                       <channel id="doorOpen" typeId="doorOpen"/>
+                       <channel id="fanSpeed" typeId="fanSpeed"/>
+                       <channel id="workMode" typeId="workMode"/>
+                       <channel id="ionizer" typeId="ionizer"/>
+                       <channel id="status" typeId="status"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Electrolux</property>
+               </properties>
+
+               <representation-property>deviceId</representation-property>
+
+               <config-description>
+                       <parameter name="deviceId" type="text" required="true">
+                               <label>Device Id</label>
+                               <description>Unique Id.</description>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+       <channel-type id="status">
+               <item-type>String</item-type>
+               <label>Current Status</label>
+               <description>Information on current status.</description>
+               <state readOnly="true" pattern="%s"/>
+       </channel-type>
+
+       <channel-type id="temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>Temperature</description>
+               <category>Temperature</category>
+               <state readOnly="true" pattern="%.1f %unit%">
+               </state>
+       </channel-type>
+
+       <channel-type id="humidity">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidity</label>
+               <description>Humidity</description>
+               <category>Humidity</category>
+               <state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="tvoc">
+               <item-type>Number:Density</item-type>
+               <label>TVOC</label>
+               <description>Total Volatile Organic Compounds</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+
+       <channel-type id="pm1">
+               <item-type>Number:Dimensionless</item-type>
+               <label>PM1</label>
+               <description>Particulate Matter 1 (0.001mm)</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="pm2_5">
+               <item-type>Number:Dimensionless</item-type>
+               <label>PM2.5</label>
+               <description>Particulate Matter 2.5 (0.0025mm)</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="pm10">
+               <item-type>Number:Dimensionless</item-type>
+               <label>PM10</label>
+               <description>Particulate Matter 10 (0.01mm)</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="co2">
+               <item-type>Number:Dimensionless</item-type>
+               <label>CO2</label>
+               <description>CarbonDioxide</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="filterLife">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Filter Life</label>
+               <description>Filter Life</description>
+               <state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="doorOpen">
+               <item-type>Contact</item-type>
+               <label>Door Status</label>
+               <description>Door Status Open/Closed</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="fanSpeed">
+               <item-type>Number</item-type>
+               <label>Fan Speed Setting</label>
+               <description>Fan Speed Setting</description>
+               <state>
+                       <options>
+                               <option value="1">Level 1</option>
+                               <option value="2">Level 2</option>
+                               <option value="3">Level 3</option>
+                               <option value="4">Level 4</option>
+                               <option value="5">Level 5</option>
+                               <option value="6">Level 6</option>
+                               <option value="7">Level 7</option>
+                               <option value="8">Level 8</option>
+                               <option value="9">Level 9</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="workMode">
+               <item-type>String</item-type>
+               <label>Work Mode Setting</label>
+               <description>Work Mode Setting</description>
+               <state>
+                       <options>
+                               <option value="PowerOff">Power Off</option>
+                               <option value="Auto">Automatic</option>
+                               <option value="Manual">Manual</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="ionizer">
+               <item-type>Switch</item-type>
+               <label>Ionizer Status</label>
+               <description>Ionizer Status</description>
+       </channel-type>
+
+
+</thing:thing-descriptions>
index e3469531f409ff7d819277e9a2705976197d4f0e..536eef33390ec63b98645c8747bac1603b8121b7 100644 (file)
     <module>org.openhab.binding.ecobee</module>
     <module>org.openhab.binding.ecotouch</module>
     <module>org.openhab.binding.ekey</module>
+    <module>org.openhab.binding.electroluxair</module>
     <module>org.openhab.binding.elerotransmitterstick</module>
     <module>org.openhab.binding.energenie</module>
     <module>org.openhab.binding.enigma2</module>