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