[electroluxair] Initial contribution (#11116)

* [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>
This commit is contained in:
Jan Gustafsson 2022-01-09 10:02:25 +01:00 committed by GitHub
parent 06576fa0fe
commit f79ce45650
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1917 additions and 0 deletions

View 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

View File

@ -391,6 +391,11 @@
<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>

View File

@ -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

View File

@ -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"}
````

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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 "";
}
}

View File

@ -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 = "";
}
}

View File

@ -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();
}
}

View File

@ -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 = "";
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>

View File

@ -112,6 +112,7 @@
<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>