mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[myuplink] Initial contribution (#17451)
* myuplink skeleton Signed-off-by: Alexander Friese <af944580@googlemail.com> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
546b58e0da
commit
dcc8cb85cb
@ -246,6 +246,7 @@
|
||||
/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @martingrassl
|
||||
/bundles/org.openhab.binding.mynice/ @clinique
|
||||
/bundles/org.openhab.binding.mystrom/ @pail23
|
||||
/bundles/org.openhab.binding.myuplink/ @alexf2015
|
||||
/bundles/org.openhab.binding.nanoleaf/ @stefan-hoehn
|
||||
/bundles/org.openhab.binding.neato/ @jjlauterbach
|
||||
/bundles/org.openhab.binding.neeo/ @morph166955
|
||||
|
@ -1236,6 +1236,11 @@
|
||||
<artifactId>org.openhab.binding.mystrom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.myuplink</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.nanoleaf</artifactId>
|
||||
|
13
bundles/org.openhab.binding.myuplink/NOTICE
Normal file
13
bundles/org.openhab.binding.myuplink/NOTICE
Normal 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
|
78
bundles/org.openhab.binding.myuplink/README.md
Normal file
78
bundles/org.openhab.binding.myuplink/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# myUplink Binding
|
||||
|
||||
The myUplink binding is used to get "live data" from from Nibe heat pumps without plugging any custom devices into your heat pump.
|
||||
This avoids the risk of losing your warranty.
|
||||
Instead data is retrieved from myUplink.
|
||||
The myUplink API is the successor of the Nibe Uplink API.
|
||||
This binding should in general be compatible with all heat pump models that support myUplink.
|
||||
Read or write access is supported by all channels as exposed by the API.
|
||||
Write access might only be available with a paid subscription for myUplink.
|
||||
You will need to create credentials at <https://dev.myuplink.com/apps> in order to use this binding.
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding provides two thing types:
|
||||
|
||||
| Thing/Bridge | Thing Type | Description |
|
||||
|---------------------|---------------------|-------------------------------------------------------------------|
|
||||
| bridge | account | cloud connection to a myUplink user account |
|
||||
| thing | generic-device | the physical heatpump which is connected to myUplink |
|
||||
## Discovery
|
||||
|
||||
When the `account` bridge is setup, the binding will discover all heatpumps within that account and also detect the specific channels supported by the model.
|
||||
|
||||
## Bridge Configuration
|
||||
|
||||
The following configuration parameters are available for the bridge:
|
||||
|
||||
| Configuration Parameter | Required | Description |
|
||||
|-------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| clientId | yes | The clientId to login at myUplink cloud service. This is some kind of UUID. Visit <https://dev.myuplink.com/apps> to generate login credentials. |
|
||||
| clientSecret | yes | The secret which belongs to the clientId. |
|
||||
| dataPollingInterval | no | Interval (seconds) in which live data values are retrieved from the Easee Cloud API. (default = 60) |
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
It is recommended to use auto discovery which does not require further configuration.
|
||||
If manual configuration is preferred you need to specify configuration as below.
|
||||
|
||||
| Configuration Parameter | Required | Description |
|
||||
|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------|
|
||||
| deviceId | yes | The id of the heatpump that will be represented by this thing. Can be retrieved via API call or autodiscovery. |
|
||||
| systemId | no | The systemId of the heatpump. Only needed for "SmartHomeMode". Can be retrieved via API call or autodiscovery. |
|
||||
|
||||
## Channels
|
||||
|
||||
The binding only supports channels which are explicitely exposed by the myUplink API.
|
||||
|
||||
Depending on your model and additional hardware the channels might be different.
|
||||
Thus no list is provided here.
|
||||
|
||||
## Full Example
|
||||
|
||||
The configuration below is an example which could easily be adopted to your actual model.
|
||||
Thing configuration (account and generic-device) is the same for all models.
|
||||
Item configuration depends on your specific model and thus channels will have different IDs and/or channels might not exist for all models.
|
||||
|
||||
### `demo.things` Example
|
||||
|
||||
```java
|
||||
Bridge myuplink:account:myAccount "myUplink" [
|
||||
clientId="c7c2f9a4-b960-448f-b00d-b8f30aff3324",
|
||||
clientSecret="471147114711ABCDEF133713371337AB",
|
||||
dataPollingInterval=55
|
||||
] {
|
||||
Thing generic-device vvm320 "VVM320" [ deviceId="id taken from automatic discovery", systemId="id taken from automatic discovery" ]
|
||||
}
|
||||
```
|
||||
|
||||
### `demo.items` Example
|
||||
|
||||
```java
|
||||
Number NIBE_ADD_STATUS "Status ZH [%s]" { channel="myuplink:generic-device:myAccount:vvm320:49993" }
|
||||
Number NIBE_COMP_STATUS "Status Compr. [%s]" { channel="myuplink:generic-device:myAccount:vvm320:44064" }
|
||||
Number:Temperature NIBE_SUPPLY "Supply line" { unit="°C", channel="myuplink:generic-device:myAccount:vvm320:40008" }
|
||||
Number:Temperature NIBE_RETURN "Return line" { unit="°C", channel="myuplink:generic-device:myAccount:vvm320:40012" }
|
||||
Number:Energy NIBE_HM_HEAT "HM heating" { unit="kWh", channel="myuplink:generic-device:myAccount:vvm320:44308" }
|
||||
Number:Energy NIBE_HM_HW "HM hot water" { unit="kWh", channel="myuplink:generic-device:myAccount:vvm320:44306" }
|
||||
```
|
17
bundles/org.openhab.binding.myuplink/pom.xml
Normal file
17
bundles/org.openhab.binding.myuplink/pom.xml
Normal 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 https://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>4.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.myuplink</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: myUplink Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.myuplink-${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-myuplink" description="myUplink Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.myuplink/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* trait class which contains useful helper methods. Thus, the interface can be implemented and methods are available
|
||||
* within the class.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AtomicReferenceTrait {
|
||||
|
||||
/**
|
||||
* this should usually not called directly. use updateJobReference or cancelJobReference instead
|
||||
*
|
||||
* @param job job to cancel.
|
||||
*/
|
||||
default void cancelJob(@Nullable Future<?> job) {
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* updates a job reference with a new job. the old job will be cancelled if there is one.
|
||||
*
|
||||
* @param jobReference reference to be updated
|
||||
* @param newJob job to be assigned
|
||||
*/
|
||||
default void updateJobReference(AtomicReference<@Nullable Future<?>> jobReference, Future<?> newJob) {
|
||||
cancelJob(jobReference.getAndSet(newJob));
|
||||
}
|
||||
|
||||
/**
|
||||
* updates a job reference to null and cancels any existing job which might be assigned to the reference.
|
||||
*
|
||||
* @param jobReference to be updated to null.
|
||||
*/
|
||||
default void cancelJobReference(AtomicReference<@Nullable Future<?>> jobReference) {
|
||||
cancelJob(jobReference.getAndSet(null));
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link MyUplinkBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Alexander Friese - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MyUplinkBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "myuplink";
|
||||
|
||||
// List of main device types
|
||||
public static final String DEVICE_ACCOUNT = "account";
|
||||
public static final String DEVICE_GENERIC_DEVICE = "generic-device";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, DEVICE_ACCOUNT);
|
||||
public static final ThingTypeUID THING_TYPE_GENERIC_DEVICE = new ThingTypeUID(BINDING_ID, DEVICE_GENERIC_DEVICE);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
|
||||
THING_TYPE_GENERIC_DEVICE);
|
||||
|
||||
// Channel types
|
||||
public static final String CHANNEL_TYPE_UNIT_NONE = "NO_UNIT";
|
||||
public static final String CHANNEL_TYPE_PREFIX_RW = "rw";
|
||||
public static final String CHANNEL_TYPE_ENUM_PRFIX = "type-enum-";
|
||||
public static final String CHANNEL_TYPE_NUMERIC_PRFIX = "type-numeric-";
|
||||
public static final String CHANNEL_TYPE_DEFAULT_DATATYPE = "Number";
|
||||
|
||||
public static final String CHANNEL_TYPE_ENERGY = "type-energy";
|
||||
public static final String CHANNEL_TYPE_ENERGY_UNIT = "kWh";
|
||||
public static final String CHANNEL_TYPE_PRESSURE = "type-pressure";
|
||||
public static final String CHANNEL_TYPE_PRESSURE_UNIT = "bar";
|
||||
public static final String CHANNEL_TYPE_PERCENT = "type-percent";
|
||||
public static final String CHANNEL_TYPE_PERCENT_UNIT = "%";
|
||||
public static final String CHANNEL_TYPE_TEMPERATURE = "type-temperature";
|
||||
public static final String CHANNEL_TYPE_TEMPERATURE_UNIT = "°C";
|
||||
public static final String CHANNEL_TYPE_FREQUENCY = "type-frequency";
|
||||
public static final String CHANNEL_TYPE_FREQUENCY_UNIT = "Hz";
|
||||
public static final String CHANNEL_TYPE_FLOW = "type-flow";
|
||||
public static final String CHANNEL_TYPE_FLOW_UNIT = "l/m";
|
||||
public static final String CHANNEL_TYPE_ELECTRIC_CURRENT = "type-electric-current";
|
||||
public static final String CHANNEL_TYPE_ELECTRIC_CURRENT_UNIT = "A";
|
||||
public static final String CHANNEL_TYPE_TIME = "type-time";
|
||||
public static final String CHANNEL_TYPE_TIME_UNIT = "h";
|
||||
public static final String CHANNEL_TYPE_INTEGER = "type-number-integer";
|
||||
public static final String CHANNEL_TYPE_DOUBLE = "type-number-double";
|
||||
public static final String CHANNEL_TYPE_ON_OFF = "type-on-off";
|
||||
public static final String CHANNEL_TYPE_RW_SWITCH = "rwtype-switch";
|
||||
public static final String CHANNEL_TYPE_RW_COMMAND = "rwtype-command";
|
||||
public static final String CHANNEL_TYPE_RW_MODE = "rwtype-mode";
|
||||
|
||||
public static final String CHANNEL_ID_COMMAND = "command";
|
||||
public static final String CHANNEL_ID_SMART_HOME_MODE = "smart-home-mode";
|
||||
|
||||
// JSON Keys
|
||||
public static final String JSON_KEY_ROOT_DATA = "data";
|
||||
public static final String JSON_KEY_CHANNEL_STR_VAL = "strVal";
|
||||
public static final String JSON_KEY_CHANNEL_VALUE = "value";
|
||||
public static final String JSON_KEY_CHANNEL_WRITABLE = "writable";
|
||||
public static final String JSON_KEY_CHANNEL_ENUM_VALUES = "enumValues";
|
||||
public static final String JSON_KEY_CHANNEL_ID = "parameterId";
|
||||
public static final String JSON_KEY_CHANNEL_LABEL = "parameterName";
|
||||
public static final String JSON_KEY_CHANNEL_UNIT = "parameterUnit";
|
||||
public static final String JSON_KEY_CHANNEL_SCALE = "scaleValue";
|
||||
public static final String JSON_KEY_CHANNEL_MIN = "minValue";
|
||||
public static final String JSON_KEY_CHANNEL_MAX = "maxValue";
|
||||
public static final String JSON_KEY_CHANNEL_STEP = "stepValue";
|
||||
public static final String JSON_KEY_SYSTEMS = "systems";
|
||||
public static final String JSON_KEY_SYSTEM_ID = "systemId";
|
||||
public static final String JSON_KEY_DEVICES = "devices";
|
||||
public static final String JSON_KEY_GENERIC_ID = "id";
|
||||
public static final String JSON_KEY_PRODUCT = "product";
|
||||
public static final String JSON_KEY_SERIAL = "serialNumber";
|
||||
public static final String JSON_KEY_NAME = "name";
|
||||
public static final String JSON_KEY_CURRENT_FW_VERSION = "currentFwVersion";
|
||||
public static final String JSON_KEY_CONNECTION_STATE = "connectionState";
|
||||
public static final String JSON_KEY_ERROR = "error";
|
||||
public static final String JSON_KEY_SMART_HOME_MODE = "smartHomeMode";
|
||||
|
||||
public static final String JSON_KEY_AUTH_ACCESS_TOKEN = "access_token";
|
||||
public static final String JSON_KEY_AUTH_EXPIRES_IN = "expires_in";
|
||||
|
||||
public static final String JSON_ENUM_KEY_TEXT = "text";
|
||||
public static final String JSON_ENUM_ORD_0 = "0";
|
||||
public static final String JSON_ENUM_ORD_1 = "1";
|
||||
public static final String JSON_ENUM_ORD_4 = "4";
|
||||
public static final String JSON_ENUM_ORD_6 = "6";
|
||||
public static final String JSON_ENUM_ORD_10 = "10";
|
||||
public static final String JSON_ENUM_ORD_20 = "20";
|
||||
public static final String JSON_ENUM_ORD_30 = "30";
|
||||
public static final String JSON_ENUM_ORD_40 = "40";
|
||||
public static final String JSON_ENUM_ORD_60 = "60";
|
||||
public static final String JSON_ENUM_ORD_100 = "100";
|
||||
public static final String JSON_ENUM_VAL_OFF = "off";
|
||||
public static final String JSON_ENUM_VAL_ON = "on";
|
||||
public static final String JSON_ENUM_VAL_HOT_WATER = "hot water";
|
||||
public static final String JSON_ENUM_VAL_HEATING = "heating";
|
||||
public static final String JSON_ENUM_VAL_POOL = "pool";
|
||||
public static final String JSON_ENUM_VAL_STARTS = "starts";
|
||||
public static final String JSON_ENUM_VAL_RUNS = "runs";
|
||||
public static final String JSON_ENUM_VAL_ALARM = "alarm";
|
||||
public static final String JSON_ENUM_VAL_BLOCKED = "blocked";
|
||||
public static final String JSON_ENUM_VAL_ACTIVE = "active";
|
||||
|
||||
public static final String JSON_VAL_CONNECTION_CONNECTED = "Connected";
|
||||
public static final String JSON_VAL_DECIMAL_SEPARATOR = ".";
|
||||
|
||||
// web request constants
|
||||
public static final long WEB_REQUEST_INITIAL_DELAY = 10;
|
||||
public static final long WEB_REQUEST_INTERVAL = 5;
|
||||
public static final int WEB_REQUEST_QUEUE_MAX_SIZE = 20;
|
||||
public static final int WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES = 5;
|
||||
public static final int WEB_REQUEST_TOKEN_MAX_AGE_MINUTES = 45;
|
||||
public static final String WEB_REQUEST_PARAM_PAGE_KEY = "page";
|
||||
public static final String WEB_REQUEST_PARAM_PAGE_SIZE_KEY = "itemsPerPage";
|
||||
public static final String WEB_REQUEST_PATCH_CONTENT_TYPE = "application/json-patch+json";
|
||||
public static final int WEB_REQUEST_PARAM_PAGE_SIZE_VALUE = 100;
|
||||
public static final String WEB_REQUEST_BEARER_TOKEN_PREFIX = "Bearer ";
|
||||
public static final String LOGIN_BASIC_AUTH_PREFIX = "Basic ";
|
||||
public static final String LOGIN_FIELD_SCOPE_KEY = "scope";
|
||||
public static final String LOGIN_FIELD_SCOPE_VALUE = "READSYSTEM WRITESYSTEM";
|
||||
public static final String LOGIN_FIELD_GRANT_TYPE_KEY = "grant_type";
|
||||
public static final String LOGIN_FIELD_GRANT_TYPE_VALUE = "client_credentials";
|
||||
|
||||
// URLs
|
||||
private static final String API_BASE_URL = "https://api.myuplink.com";
|
||||
public static final String LOGIN_URL = API_BASE_URL + "/oauth/token";
|
||||
public static final String GET_SYSTEMS_URL = API_BASE_URL + "/v2/systems/me";
|
||||
public static final String GET_SMART_HOME_MODE_URL = API_BASE_URL + "/v2/systems/{systemId}/smart-home-mode";
|
||||
public static final String SET_SMART_HOME_MODE_URL = GET_SMART_HOME_MODE_URL;
|
||||
public static final String GET_DEVICE_POINTS = API_BASE_URL + "/v2/devices/{deviceId}/points";
|
||||
public static final String SET_DEVICE_POINTS = GET_DEVICE_POINTS;
|
||||
|
||||
// Status Keys
|
||||
public static final String STATUS_TOKEN_VALIDATED = "@text/status.token.validated";
|
||||
public static final String STATUS_WAITING_FOR_BRIDGE = "@text/status.waiting.for.bridge";
|
||||
public static final String STATUS_WAITING_FOR_LOGIN = "@text/status.waiting.for.login";
|
||||
public static final String STATUS_NO_VALID_DATA = "@text/status.no.valid.data";
|
||||
public static final String STATUS_NO_CONNECTION = "@text/status.no.connection";
|
||||
public static final String STATUS_DEVICE_NOT_FOUND = "@text/status.device.not.found";
|
||||
public static final String STATUS_CONFIG_ERROR_NO_CLIENT_ID = "@text/status.config.error.no.client.id";
|
||||
public static final String STATUS_CONFIG_ERROR_NO_CLIENT_SECRET = "@text/status.config.error.no.client.secret";
|
||||
|
||||
// other
|
||||
public static final long POLLING_INITIAL_DELAY = 5;
|
||||
|
||||
public static final String GENERIC_NO_VAL = "---";
|
||||
public static final String EMPTY = "";
|
||||
|
||||
public static final String THING_CONFIG_ID = "deviceId";
|
||||
public static final String THING_CONFIG_SYSTEM_ID = "systemId";
|
||||
public static final String THING_CONFIG_SERIAL = "serial";
|
||||
public static final String THING_CONFIG_CURRENT_FW_VERSION = "currentFwVersion";
|
||||
|
||||
public static final Instant OUTDATED_DATE = Instant.EPOCH;
|
||||
|
||||
public static final String PARAMETER_NAME_WRITE_COMMAND = "writeCommand";
|
||||
public static final String PARAMETER_NAME_VALIDATION_REGEXP = "validationExpression";
|
||||
public static final String DEFAULT_VALIDATION_EXPRESSION = "[0-9]+";
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkAccountHandler;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkGenericDeviceHandler;
|
||||
import org.openhab.binding.myuplink.internal.provider.ChannelFactory;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link myUplinkHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Alexander Friese - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.myuplink", service = ThingHandlerFactory.class)
|
||||
public class MyUplinkHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MyUplinkHandlerFactory.class);
|
||||
|
||||
/**
|
||||
* the shared http client
|
||||
*/
|
||||
private final HttpClient httpClient;
|
||||
|
||||
private final ChannelFactory channelFactory;
|
||||
|
||||
@Activate
|
||||
public MyUplinkHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference ChannelFactory channelFactory) {
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.channelFactory = channelFactory;
|
||||
}
|
||||
|
||||
@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_ACCOUNT.equals(thingTypeUID)) {
|
||||
return new MyUplinkAccountHandler((Bridge) thing, httpClient);
|
||||
} else if (THING_TYPE_GENERIC_DEVICE.equals(thingTypeUID)) {
|
||||
return new MyUplinkGenericDeviceHandler(thing, channelFactory);
|
||||
} else {
|
||||
logger.warn("Unsupported Thing-Type: {}", thingTypeUID.getAsString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.myuplink.internal.model.ConfigurationException;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
/**
|
||||
* some helper methods.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class Utils {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
|
||||
|
||||
/**
|
||||
* only static methods no instance needed
|
||||
*/
|
||||
private Utils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* parses a date string in api source format to ZonedDateTime which is used by openHAB.
|
||||
*
|
||||
* @param date
|
||||
* @return
|
||||
*/
|
||||
public static ZonedDateTime parseDate(String date) throws DateTimeParseException {
|
||||
DateTimeFormatter formatter;
|
||||
if (date.length() == 24) {
|
||||
formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
|
||||
} else {
|
||||
formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX");
|
||||
}
|
||||
LOGGER.trace("parsing: {}", date);
|
||||
return ZonedDateTime.parse(date, formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* get element as JsonObject.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public static @Nullable JsonObject getAsJsonObject(@Nullable JsonObject jsonObject, String key) {
|
||||
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
|
||||
return (element instanceof JsonObject) ? element.getAsJsonObject() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* get element as JsonArray.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public static JsonArray getAsJsonArray(@Nullable JsonObject jsonObject, String key) {
|
||||
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
|
||||
return (element instanceof JsonArray) ? element.getAsJsonArray() : new JsonArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* get element as String.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public static @Nullable String getAsString(@Nullable JsonObject jsonObject, String key) {
|
||||
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
|
||||
String text = null;
|
||||
if (element != null) {
|
||||
if (element instanceof JsonPrimitive) {
|
||||
text = element.getAsString();
|
||||
} else if (element instanceof JsonObject || element instanceof JsonArray) {
|
||||
text = element.toString();
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* null safe version of getAsString with default value.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @param defaultVal
|
||||
* @return
|
||||
*/
|
||||
public static String getAsString(@Nullable JsonObject jsonObject, String key, String defaultVal) {
|
||||
String text = getAsString(jsonObject, key);
|
||||
return text == null ? defaultVal : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* get element as int.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public static int getAsInt(@Nullable JsonObject jsonObject, String key) {
|
||||
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
|
||||
return (element instanceof JsonPrimitive) ? element.getAsInt() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* get element as BigDecimal.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public static @Nullable BigDecimal getAsBigDecimal(@Nullable JsonObject jsonObject, String key) {
|
||||
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
|
||||
return (element == null || element instanceof JsonNull) ? null : element.getAsBigDecimal();
|
||||
}
|
||||
|
||||
/**
|
||||
* get element as boolean.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public static @Nullable Boolean getAsBool(@Nullable JsonObject jsonObject, String key) {
|
||||
JsonElement json = jsonObject == null ? null : jsonObject.get(key);
|
||||
return (json == null || json instanceof JsonNull) ? null : json.getAsBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* null safe version of getAsBool with default value.
|
||||
*
|
||||
* @param jsonObject
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public static boolean getAsBool(@Nullable JsonObject jsonObject, String key, Boolean defaultValue) {
|
||||
Boolean result = getAsBool(jsonObject, key);
|
||||
return result == null ? defaultValue : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves typeID of a channel.
|
||||
*
|
||||
* @param channel
|
||||
* @return typeID or empty string if typeUID is null.
|
||||
*/
|
||||
public static String getChannelTypeId(Channel channel) {
|
||||
ChannelTypeUID typeUID = channel.getChannelTypeUID();
|
||||
if (typeUID == null) {
|
||||
return "";
|
||||
}
|
||||
return typeUID.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves the validation expression which is assigned to this channel, fallback to a public static, if no
|
||||
* validation
|
||||
* is
|
||||
* defined.
|
||||
*
|
||||
* @param channel
|
||||
* @return the validation expression
|
||||
*/
|
||||
public static String getValidationExpression(Channel channel) {
|
||||
String expr = getPropertyOrParameter(channel, PARAMETER_NAME_VALIDATION_REGEXP);
|
||||
if (expr == null) {
|
||||
throw new ConfigurationException(
|
||||
"channel (" + channel.getUID().getId() + ") does not have a validation expression configured");
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* internal utiliy method which returns a property (if found) or a config parameter (if found) otherwise null
|
||||
*
|
||||
* @param channel
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public static @Nullable String getPropertyOrParameter(Channel channel, String name) {
|
||||
String value = channel.getProperties().get(name);
|
||||
// also eclipse says this cannot be null, it definitely can!
|
||||
if (value == null || value.isEmpty()) {
|
||||
Object obj = channel.getConfiguration().get(name);
|
||||
value = obj == null ? null : obj.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* converts units received from myUplink API into UoM compliant strings.
|
||||
*
|
||||
* @param originalUnit
|
||||
* @return UoM compliant unit
|
||||
*/
|
||||
public static String fixUnit(String originalUnit) {
|
||||
return switch (originalUnit) {
|
||||
case "l/m" -> "l/min";
|
||||
case "hrs" -> "h";
|
||||
case "m3/h" -> "m³/h";
|
||||
case "days" -> "d";
|
||||
default -> originalUnit;
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.client.util.BufferingResponseListener;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpStatus.Code;
|
||||
import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.GenericResponseTransformer;
|
||||
import org.openhab.binding.myuplink.internal.model.ResponseTransformer;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.ToNumberPolicy;
|
||||
|
||||
/**
|
||||
* base class for all commands. common logic should be implemented here
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractCommand extends BufferingResponseListener implements MyUplinkCommand {
|
||||
|
||||
public enum RetryOnFailure {
|
||||
YES,
|
||||
NO
|
||||
}
|
||||
|
||||
public enum ProcessFailureResponse {
|
||||
YES,
|
||||
NO
|
||||
}
|
||||
|
||||
/**
|
||||
* logger
|
||||
*/
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractCommand.class);
|
||||
|
||||
/**
|
||||
* the configuration
|
||||
*/
|
||||
protected final MyUplinkThingHandler handler;
|
||||
|
||||
/**
|
||||
* JSON deserializer
|
||||
*/
|
||||
protected final Gson gson;
|
||||
|
||||
/**
|
||||
* status code of fulfilled request
|
||||
*/
|
||||
private final CommunicationStatus communicationStatus;
|
||||
|
||||
/**
|
||||
* generic transformer which just transfers all values in a plain map.
|
||||
*/
|
||||
protected final ResponseTransformer transformer;
|
||||
|
||||
/**
|
||||
* retry counter.
|
||||
*/
|
||||
private int retries = 0;
|
||||
|
||||
/**
|
||||
* retry active
|
||||
*/
|
||||
private final RetryOnFailure retryOnFailure;
|
||||
|
||||
/**
|
||||
* process error response, e.g. set handler offline on error
|
||||
*/
|
||||
private final ProcessFailureResponse processFailureResponse;
|
||||
|
||||
/**
|
||||
* allows further processing of the json result data, if set.
|
||||
*/
|
||||
private final JsonResultProcessor resultProcessor;
|
||||
|
||||
/**
|
||||
* the constructor
|
||||
*/
|
||||
public AbstractCommand(MyUplinkThingHandler handler, RetryOnFailure retryOnFailure,
|
||||
ProcessFailureResponse processFailureResponse, JsonResultProcessor resultProcessor) {
|
||||
this(handler, new GenericResponseTransformer(handler), retryOnFailure, processFailureResponse, resultProcessor);
|
||||
}
|
||||
|
||||
public AbstractCommand(MyUplinkThingHandler handler, ResponseTransformer responseTransformer,
|
||||
RetryOnFailure retryOnFailure, ProcessFailureResponse processFailureResponse,
|
||||
JsonResultProcessor resultProcessor) {
|
||||
this.gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
|
||||
this.communicationStatus = new CommunicationStatus();
|
||||
this.transformer = responseTransformer;
|
||||
this.handler = handler;
|
||||
this.processFailureResponse = processFailureResponse;
|
||||
this.retryOnFailure = retryOnFailure;
|
||||
this.resultProcessor = resultProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log request success
|
||||
*/
|
||||
@Override
|
||||
public final void onSuccess(@Nullable Response response) {
|
||||
if (response != null) {
|
||||
super.onSuccess(response);
|
||||
communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
|
||||
logger.debug("[{}] HTTP response {}", getClass().getSimpleName(), response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log request failure
|
||||
*/
|
||||
@Override
|
||||
public final void onFailure(@Nullable Response response, @Nullable Throwable failure) {
|
||||
if (failure != null && response != null) {
|
||||
super.onFailure(response, failure);
|
||||
}
|
||||
if (failure != null) {
|
||||
logger.info("[{}] Request failed: {}", getClass().getSimpleName(), failure.toString());
|
||||
communicationStatus.setError((Exception) failure);
|
||||
if (failure instanceof SocketTimeoutException || failure instanceof TimeoutException) {
|
||||
communicationStatus.setHttpCode(Code.REQUEST_TIMEOUT);
|
||||
} else if (failure instanceof UnknownHostException) {
|
||||
communicationStatus.setHttpCode(Code.BAD_GATEWAY);
|
||||
} else {
|
||||
communicationStatus.setHttpCode(Code.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} else {
|
||||
logger.warn("[{}] Request failed", getClass().getSimpleName());
|
||||
}
|
||||
if (response != null && response.getStatus() > 0) {
|
||||
communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* just for logging of content
|
||||
*/
|
||||
@Override
|
||||
public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
|
||||
if (response != null && content != null) {
|
||||
super.onContent(response, content);
|
||||
}
|
||||
var contentAsString = getContentAsString();
|
||||
var contentLength = contentAsString == null ? 0 : contentAsString.length();
|
||||
logger.debug("[{}] received content, length: {}, encoding: {}", getClass().getSimpleName(), contentLength,
|
||||
this.getEncoding());
|
||||
}
|
||||
|
||||
/**
|
||||
* default handling of successful requests.
|
||||
*/
|
||||
@Override
|
||||
public void onComplete(@Nullable Result result) {
|
||||
String json = getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
logger.debug("[{}] JSON String: {}", getClass().getSimpleName(), json);
|
||||
switch (getCommunicationStatus().getHttpCode()) {
|
||||
case OK:
|
||||
case ACCEPTED:
|
||||
onCompleteCodeOk(json);
|
||||
break;
|
||||
default:
|
||||
onCompleteCodeDefault(json);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handling of result in case of HTTP response OK.
|
||||
*
|
||||
* @param json
|
||||
*/
|
||||
protected void onCompleteCodeOk(@Nullable String json) {
|
||||
JsonObject jsonObject = transform(json);
|
||||
if (jsonObject != null) {
|
||||
logger.debug("[{}] success", getClass().getSimpleName());
|
||||
handler.updateChannelStatus(transformer.transform(jsonObject, getChannelGroup()));
|
||||
processResult(jsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handling of result in default case, this means error handling of http codes where no specific handling applies.
|
||||
*
|
||||
* @param json
|
||||
*/
|
||||
protected void onCompleteCodeDefault(@Nullable String json) {
|
||||
JsonObject jsonObject = transform(json);
|
||||
if (jsonObject == null) {
|
||||
jsonObject = new JsonObject();
|
||||
}
|
||||
if (processFailureResponse == ProcessFailureResponse.YES) {
|
||||
processResult(jsonObject);
|
||||
} else {
|
||||
logger.warn("command failed, url: {} - code: {} - result: {}", getURL(),
|
||||
getCommunicationStatus().getHttpCode(), jsonObject.toString());
|
||||
}
|
||||
|
||||
if (retryOnFailure == RetryOnFailure.YES && retries++ < MAX_RETRIES) {
|
||||
handler.enqueueCommand(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* error safe json transformer.
|
||||
*
|
||||
* @param json
|
||||
* @return
|
||||
*/
|
||||
protected @Nullable JsonObject transform(@Nullable String json) {
|
||||
if (json != null) {
|
||||
try {
|
||||
JsonElement jsonElement = gson.fromJson(json, JsonElement.class);
|
||||
JsonObject jsonObject;
|
||||
if (jsonElement instanceof JsonObject) {
|
||||
jsonObject = jsonElement.getAsJsonObject();
|
||||
} else {
|
||||
jsonObject = new JsonObject();
|
||||
jsonObject.add(JSON_KEY_ROOT_DATA, jsonElement);
|
||||
}
|
||||
return jsonObject;
|
||||
} catch (Exception ex) {
|
||||
logger.debug("[{}] JSON could not be parsed: {}\nError: {}", getClass().getSimpleName(), json,
|
||||
ex.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* preparation of the request. will call a hook (prepareRequest) that has to be implemented in the subclass to add
|
||||
* content to the request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
@Override
|
||||
public void performAction(HttpClient asyncclient, String accessToken) throws ValidationException {
|
||||
Request request = asyncclient.newRequest(getURL()).timeout(handler.getBridgeConfiguration().getAsyncTimeout(),
|
||||
TimeUnit.SECONDS);
|
||||
logger.debug("[{}] running command", getClass().getSimpleName());
|
||||
|
||||
// we want to receive json only, so explicitely set this!
|
||||
request.header(HttpHeader.ACCEPT, "application/json");
|
||||
request.header(HttpHeader.ACCEPT_ENCODING, StandardCharsets.UTF_8.name());
|
||||
|
||||
// this should be the default for myUplink Cloud API
|
||||
request.followRedirects(false);
|
||||
|
||||
// add authentication data for every request. Handling this here makes it obsolete to implement for each and
|
||||
// every command
|
||||
if (!accessToken.isBlank()) {
|
||||
request.header(HttpHeader.AUTHORIZATION, WEB_REQUEST_BEARER_TOKEN_PREFIX + accessToken);
|
||||
}
|
||||
|
||||
prepareRequest(request).send(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return returns Http Status Code
|
||||
*/
|
||||
public CommunicationStatus getCommunicationStatus() {
|
||||
return communicationStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* calls the registered resultProcessor.
|
||||
*
|
||||
* @param jsonObject
|
||||
*/
|
||||
protected final void processResult(JsonObject jsonObject) {
|
||||
try {
|
||||
resultProcessor.processResult(getCommunicationStatus(), jsonObject);
|
||||
} catch (Exception ex) {
|
||||
// this should not happen
|
||||
logger.warn("[{}] Exception caught: {}", getClass().getSimpleName(), ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* default implementation just assumes that we want to retrieve data via GET.
|
||||
* can be overridden for any special case and has to prepare the requests with additional parameters, etc
|
||||
*
|
||||
* @param requestToPrepare the request to prepare
|
||||
* @return prepared Request object
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected Request prepareRequest(Request requestToPrepare) throws ValidationException {
|
||||
requestToPrepare.method(HttpMethod.GET);
|
||||
return requestToPrepare;
|
||||
}
|
||||
|
||||
/**
|
||||
* concrete implementation has to provide the channel group.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected abstract String getChannelGroup();
|
||||
|
||||
/**
|
||||
* concrete implementation has to provide the URL
|
||||
*
|
||||
* @return Url
|
||||
*/
|
||||
protected abstract String getURL();
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
|
||||
/**
|
||||
* base class for all commands that support paging. common logic should be implemented here
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractPagingCommand extends AbstractCommand {
|
||||
|
||||
public AbstractPagingCommand(MyUplinkThingHandler handler, RetryOnFailure retryOnFailure,
|
||||
ProcessFailureResponse processFailureResponse, JsonResultProcessor resultProcessor) {
|
||||
super(handler, retryOnFailure, processFailureResponse, resultProcessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request prepareRequest(Request requestToPrepare) throws ValidationException {
|
||||
requestToPrepare.param(WEB_REQUEST_PARAM_PAGE_SIZE_KEY, String.valueOf(WEB_REQUEST_PARAM_PAGE_SIZE_VALUE));
|
||||
requestToPrepare.method(HttpMethod.GET);
|
||||
return requestToPrepare;
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.EMPTY;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.measure.MetricPrefix;
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* base class for all write commands. common logic should be implemented here
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractWriteCommand extends AbstractCommand {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractWriteCommand.class);
|
||||
|
||||
protected final Channel channel;
|
||||
protected final Command command;
|
||||
|
||||
/**
|
||||
* the constructor
|
||||
*/
|
||||
public AbstractWriteCommand(MyUplinkThingHandler handler, Channel channel, Command command,
|
||||
RetryOnFailure retryOnFailure, ProcessFailureResponse processFailureResponse,
|
||||
JsonResultProcessor resultProcessor) {
|
||||
super(handler, retryOnFailure, processFailureResponse, resultProcessor);
|
||||
this.channel = channel;
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
/**
|
||||
* helper method for write commands that extracts value from command.
|
||||
*
|
||||
* @return value as String without unit.
|
||||
*/
|
||||
protected String getCommandValue() {
|
||||
if (command instanceof QuantityType<?> quantityCommand) {
|
||||
// this is necessary because we must not send the unit to the backend
|
||||
Unit<?> unit = quantityCommand.getUnit();
|
||||
QuantityType<?> convertedType;
|
||||
if (unit.isCompatible(SIUnits.CELSIUS)) {
|
||||
convertedType = quantityCommand.toUnit(SIUnits.CELSIUS);
|
||||
} else if (unit.isCompatible(Units.KILOWATT_HOUR)) {
|
||||
convertedType = quantityCommand.toUnit(Units.KILOWATT_HOUR);
|
||||
} else if (unit.isCompatible(Units.LITRE_PER_MINUTE)) {
|
||||
convertedType = quantityCommand.toUnit(Units.LITRE_PER_MINUTE);
|
||||
} else if (unit.isCompatible(tech.units.indriya.unit.Units.WATT)) {
|
||||
convertedType = quantityCommand.toUnit(MetricPrefix.KILO(tech.units.indriya.unit.Units.WATT));
|
||||
} else {
|
||||
logger.warn("automatic conversion of unit '{}' to myUplink expected unit not supported.",
|
||||
unit.getName());
|
||||
convertedType = quantityCommand;
|
||||
}
|
||||
return String.valueOf(convertedType != null ? convertedType.doubleValue() : UnDefType.NULL);
|
||||
} else if (command instanceof OnOffType onOffType) {
|
||||
// this is necessary because we must send 0/1 and not ON/OFF to the backend
|
||||
return OnOffType.ON.equals(onOffType) ? "1" : "0";
|
||||
} else {
|
||||
return command.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* helper that transforms channelId + commandvalue in a JSON string that can be added as content to a POST request.
|
||||
*
|
||||
* @return converted JSON string
|
||||
*/
|
||||
protected String getJsonContent() {
|
||||
return buildJsonObject(channel.getUID().getIdWithoutGroup(), getCommandValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* helper that creates a simple json object as string.
|
||||
*
|
||||
* @param key identifier of the value
|
||||
* @param value the value to assign to the key
|
||||
*
|
||||
* @return converted JSON string
|
||||
*/
|
||||
protected String buildJsonObject(String key, String value) {
|
||||
Map<String, String> content = new HashMap<>(1);
|
||||
content.put(key, value);
|
||||
|
||||
return gson.toJson(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request prepareRequest(Request requestToPrepare) throws ValidationException {
|
||||
String channelId = channel.getUID().getIdWithoutGroup();
|
||||
String expr = Utils.getValidationExpression(channel);
|
||||
String value = getCommandValue();
|
||||
|
||||
// quantity types are transformed to double and thus we might have decimals which could cause validation error.
|
||||
// So we will shorten here in case no decimals are needed.
|
||||
if (value.endsWith(".0")) {
|
||||
value = value.substring(0, value.length() - 2);
|
||||
}
|
||||
|
||||
if (value.matches(expr)) {
|
||||
return prepareWriteRequest(requestToPrepare);
|
||||
} else {
|
||||
logger.debug("channel '{}' does not allow value '{}' - validation rule '{}'", channelId, value, expr);
|
||||
throw new ValidationException("channel (" + channelId + ") could not be updated due to a validation error");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getChannelGroup() {
|
||||
// this is a pure write command, thus no channel group needed.
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* concrete implementation has to prepare the write requests with additional parameters, etc
|
||||
*
|
||||
* @param requestToPrepare the request to prepare
|
||||
* @return prepared Request object
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected abstract Request prepareWriteRequest(Request requestToPrepare) throws ValidationException;
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* functional interface that is intended to provide a function for further result processing of json data retrieved by a
|
||||
* command.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@FunctionalInterface
|
||||
public interface JsonResultProcessor {
|
||||
|
||||
/**
|
||||
* this method processes the result of the myUplink API call.
|
||||
*
|
||||
* @param status
|
||||
* technical communication status of the http call.
|
||||
* @param jsonObject
|
||||
* json response of the http call
|
||||
*/
|
||||
void processResult(CommunicationStatus status, JsonObject jsonObject);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.Response.CompleteListener;
|
||||
import org.eclipse.jetty.client.api.Response.ContentListener;
|
||||
import org.eclipse.jetty.client.api.Response.FailureListener;
|
||||
import org.eclipse.jetty.client.api.Response.SuccessListener;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
|
||||
/**
|
||||
* public interface for all commands
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MyUplinkCommand extends SuccessListener, FailureListener, ContentListener, CompleteListener {
|
||||
|
||||
static final int MAX_RETRIES = 5;
|
||||
|
||||
/**
|
||||
* this method is to be called by the UplinkWebinterface class
|
||||
*
|
||||
* @param asyncclient
|
||||
* @throws ValidationException
|
||||
*/
|
||||
void performAction(HttpClient asyncclient, String token) throws ValidationException;
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command.account;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.openhab.binding.myuplink.internal.command.AbstractPagingCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* implements the get sites api call of the site.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetSystems extends AbstractPagingCommand {
|
||||
|
||||
public GetSystems(MyUplinkThingHandler handler, JsonResultProcessor resultProcessor) {
|
||||
// retry does not make much sense as it is a polling command, command should always succeed therefore update
|
||||
// handler on failure.
|
||||
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getURL() {
|
||||
String url = GET_SYSTEMS_URL;
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(@Nullable Result result) {
|
||||
String json = getContentAsString(StandardCharsets.UTF_8);
|
||||
JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
|
||||
|
||||
if (jsonObject != null) {
|
||||
processResult(jsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getChannelGroup() {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command.account;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.openhab.binding.myuplink.internal.command.AbstractCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkBridgeHandler;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* implements the login to the webinterface
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Login extends AbstractCommand {
|
||||
|
||||
private final String encodedLogin;
|
||||
|
||||
public Login(MyUplinkBridgeHandler handler, JsonResultProcessor resultProcessor) {
|
||||
// flags do not matter as "onComplete" is overwritten in this class.
|
||||
super(handler, RetryOnFailure.NO, ProcessFailureResponse.NO, resultProcessor);
|
||||
|
||||
String login = handler.getBridgeConfiguration().getClientId() + ":"
|
||||
+ handler.getBridgeConfiguration().getClientSecret();
|
||||
encodedLogin = Base64.getEncoder().encodeToString(login.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request prepareRequest(Request requestToPrepare) {
|
||||
Fields fields = new Fields();
|
||||
fields.add(LOGIN_FIELD_GRANT_TYPE_KEY, LOGIN_FIELD_GRANT_TYPE_VALUE);
|
||||
fields.add(LOGIN_FIELD_SCOPE_KEY, LOGIN_FIELD_SCOPE_VALUE);
|
||||
FormContentProvider cp = new FormContentProvider(fields, StandardCharsets.UTF_8);
|
||||
|
||||
requestToPrepare.header(HttpHeader.AUTHORIZATION, LOGIN_BASIC_AUTH_PREFIX + encodedLogin);
|
||||
requestToPrepare.content(cp);
|
||||
requestToPrepare.method(HttpMethod.POST);
|
||||
|
||||
return requestToPrepare;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getURL() {
|
||||
return LOGIN_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(@Nullable Result result) {
|
||||
String json = getContentAsString(StandardCharsets.UTF_8);
|
||||
JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
|
||||
|
||||
if (jsonObject != null) {
|
||||
processResult(jsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getChannelGroup() {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command.device;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.myuplink.internal.command.AbstractCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
|
||||
/**
|
||||
* implements the get points api call of the myUplink API.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetPoints extends AbstractCommand {
|
||||
private final String url;
|
||||
|
||||
public GetPoints(MyUplinkThingHandler handler, String deviceId, JsonResultProcessor resultProcessor) {
|
||||
// retry does not make much sense as it is a polling command, command should always succeed therefore update
|
||||
// handler on failure.
|
||||
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
|
||||
this.url = GET_DEVICE_POINTS.replaceAll("\\{deviceId\\}", deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getURL() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getChannelGroup() {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command.device;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.myuplink.internal.command.AbstractCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.SmartHomeModeResponseTransformer;
|
||||
|
||||
/**
|
||||
* implements the get sites api call of the site.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetSmartHomeMode extends AbstractCommand {
|
||||
private String url;
|
||||
|
||||
public GetSmartHomeMode(MyUplinkThingHandler handler, String systemId, JsonResultProcessor resultProcessor) {
|
||||
// retry does not make much sense as it is a polling command, command should always succeed therefore update
|
||||
// handler on failure.
|
||||
super(handler, new SmartHomeModeResponseTransformer(handler), RetryOnFailure.NO, ProcessFailureResponse.YES,
|
||||
resultProcessor);
|
||||
this.url = GET_SMART_HOME_MODE_URL.replaceAll("\\{systemId\\}", systemId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getURL() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getChannelGroup() {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command.device;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.myuplink.internal.command.AbstractWriteCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* implements the set points api call of the myUplink API.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SetPoints extends AbstractWriteCommand {
|
||||
private final String url;
|
||||
|
||||
public SetPoints(MyUplinkThingHandler handler, Channel channel, Command command, String deviceId,
|
||||
JsonResultProcessor resultProcessor) {
|
||||
super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES, resultProcessor);
|
||||
this.url = SET_DEVICE_POINTS.replaceAll("\\{deviceId\\}", deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getURL() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
|
||||
requestToPrepare.method(HttpMethod.PATCH);
|
||||
|
||||
StringContentProvider cp = new StringContentProvider(WEB_REQUEST_PATCH_CONTENT_TYPE, getJsonContent(),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
requestToPrepare.content(cp);
|
||||
|
||||
return requestToPrepare;
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command.device;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.myuplink.internal.command.AbstractWriteCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* implements the set points api call of the API. Extracts channel ID and value from the command string. Needed by the
|
||||
* "generic command" channel.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SetPointsAdvanced extends AbstractWriteCommand {
|
||||
private final String url;
|
||||
|
||||
public SetPointsAdvanced(MyUplinkThingHandler handler, Channel channel, Command command, String deviceId,
|
||||
JsonResultProcessor resultProcessor) {
|
||||
super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES, resultProcessor);
|
||||
this.url = SET_DEVICE_POINTS.replaceAll("\\{deviceId\\}", deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getURL() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
|
||||
requestToPrepare.method(HttpMethod.PATCH);
|
||||
|
||||
StringContentProvider cp = new StringContentProvider(WEB_REQUEST_PATCH_CONTENT_TYPE,
|
||||
buildJsonObject(getChannelId(), getChannelValue()), StandardCharsets.UTF_8);
|
||||
requestToPrepare.content(cp);
|
||||
|
||||
return requestToPrepare;
|
||||
}
|
||||
|
||||
private String getChannelId() {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
String[] tokens = stringCommand.toString().split(":");
|
||||
return tokens.length == 2 ? tokens[0] : command.toString();
|
||||
} else {
|
||||
return command.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private String getChannelValue() {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
String[] tokens = stringCommand.toString().split(":");
|
||||
return tokens.length == 2 ? tokens[1] : command.toString();
|
||||
} else {
|
||||
return command.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handling of result in case of HTTP response OK.
|
||||
*
|
||||
* @param json
|
||||
*/
|
||||
protected void onCompleteCodeOk(@Nullable String json) {
|
||||
Map<String, String> content = new HashMap<>(2);
|
||||
content.put(JSON_KEY_CHANNEL_ID, CHANNEL_ID_COMMAND);
|
||||
content.put(JSON_KEY_CHANNEL_VALUE, getCommunicationStatus().getHttpCode().name());
|
||||
content.put(JSON_KEY_ROOT_DATA, json == null ? EMPTY : json);
|
||||
|
||||
var jsonObjectString = gson.toJson(content);
|
||||
var jsonObject = gson.fromJson(jsonObjectString, JsonObject.class);
|
||||
|
||||
var jsonArray = new JsonArray();
|
||||
jsonArray.add(jsonObject);
|
||||
super.onCompleteCodeOk(gson.toJson(jsonArray));
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.command.device;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.myuplink.internal.command.AbstractWriteCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* implements the set smart home mode api call of the site.
|
||||
*
|
||||
* @author Anders Alfredsson - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SetSmartHomeMode extends AbstractWriteCommand {
|
||||
private String url;
|
||||
|
||||
public SetSmartHomeMode(MyUplinkThingHandler handler, Channel channel, Command command, String systemId,
|
||||
JsonResultProcessor resultProcessor) {
|
||||
// retry does not make much sense as it is a polling command, command should always succeed therefore update
|
||||
// handler on failure.
|
||||
super(handler, channel, command, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
|
||||
this.url = SET_SMART_HOME_MODE_URL.replaceAll("\\{systemId\\}", systemId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getURL() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
|
||||
requestToPrepare.method(HttpMethod.PUT);
|
||||
|
||||
String body = buildJsonObject(JSON_KEY_SMART_HOME_MODE, command.toString());
|
||||
|
||||
StringContentProvider cp = new StringContentProvider(WEB_REQUEST_PATCH_CONTENT_TYPE, body,
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
requestToPrepare.content(cp);
|
||||
|
||||
return requestToPrepare;
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link MyUplinkConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Alexander Friese - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MyUplinkConfiguration {
|
||||
|
||||
private String clientId = "";
|
||||
private String clientSecret = "";
|
||||
|
||||
private int dataPollingInterval = 60;
|
||||
private static final int ASYNC_TIMEOUT = 120;
|
||||
private static final int SYNC_TIMEOUT = 120;
|
||||
|
||||
/**
|
||||
* @return the clientId
|
||||
*/
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clientId the clientId to set
|
||||
*/
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the clientSecret
|
||||
*/
|
||||
public String getClientSecret() {
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clientSecret the clientSecret to set
|
||||
*/
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the asyncTimeout
|
||||
*/
|
||||
public Integer getAsyncTimeout() {
|
||||
return ASYNC_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the syncTimeout
|
||||
*/
|
||||
public Integer getSyncTimeout() {
|
||||
return SYNC_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the dataPollingInterval
|
||||
*/
|
||||
public Integer getDataPollingInterval() {
|
||||
return dataPollingInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataPollingInterval the dataPollingInterval to set
|
||||
*/
|
||||
public void setDataPollingInterval(Integer dataPollingInterval) {
|
||||
this.dataPollingInterval = dataPollingInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("MyUplinkConfiguration [clientId=").append(clientId).append(", clientSecret=")
|
||||
.append(clientSecret).append(", dataPollingInterval=").append(dataPollingInterval).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.connector;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpStatus.Code;
|
||||
|
||||
/**
|
||||
* this class contains the HTTP status of the communication and an optional exception that might occoured during
|
||||
* communication
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CommunicationStatus {
|
||||
|
||||
private @Nullable Code httpCode;
|
||||
private @Nullable Exception error;
|
||||
|
||||
public final Code getHttpCode() {
|
||||
Code code = httpCode;
|
||||
return code == null ? Code.INTERNAL_SERVER_ERROR : code;
|
||||
}
|
||||
|
||||
public final void setHttpCode(Code httpCode) {
|
||||
this.httpCode = httpCode;
|
||||
}
|
||||
|
||||
public final @Nullable Exception getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public final void setError(Exception error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public final String getMessage() {
|
||||
Exception err = error;
|
||||
String errMsg = err == null ? null : err.getMessage();
|
||||
String msg = getHttpCode().getMessage();
|
||||
if (errMsg != null && !errMsg.isEmpty()) {
|
||||
return errMsg;
|
||||
} else if (msg != null && !msg.isEmpty()) {
|
||||
return msg;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.connector;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.util.BlockingArrayQueue;
|
||||
import org.openhab.binding.myuplink.internal.AtomicReferenceTrait;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.account.Login;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkBridgeHandler;
|
||||
import org.openhab.binding.myuplink.internal.handler.StatusHandler;
|
||||
import org.openhab.binding.myuplink.internal.model.ValidationException;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The connector is responsible for communication with the NIBE myUplink Cloud API
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebInterface implements AtomicReferenceTrait {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
|
||||
|
||||
/**
|
||||
* bridge handler
|
||||
*/
|
||||
private final MyUplinkBridgeHandler handler;
|
||||
|
||||
/**
|
||||
* handler for updating bridge status
|
||||
*/
|
||||
private final StatusHandler bridgeStatusHandler;
|
||||
|
||||
/**
|
||||
* holds authentication status
|
||||
*/
|
||||
private boolean authenticated = false;
|
||||
|
||||
/**
|
||||
* access token returned by login, needed to authenticate all requests send to API.
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* expiry of the access token.
|
||||
*/
|
||||
private Instant tokenExpiry;
|
||||
|
||||
/**
|
||||
* last refresh of the access token.
|
||||
*/
|
||||
private Instant tokenRefreshDate;
|
||||
|
||||
/**
|
||||
* HTTP client for asynchronous calls
|
||||
*/
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* the scheduler which periodically sends web requests to the NIBE myUplink Cloud API. Should be initiated with the
|
||||
* thing's
|
||||
* existing scheduler instance.
|
||||
*/
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
/**
|
||||
* request executor
|
||||
*/
|
||||
private final WebRequestExecutor requestExecutor;
|
||||
|
||||
/**
|
||||
* periodic request executor job
|
||||
*/
|
||||
private final AtomicReference<@Nullable Future<?>> requestExecutorJobReference;
|
||||
|
||||
/**
|
||||
* this class is responsible for executing periodic web requests. This ensures that only one request is executed
|
||||
* at the same time and there will be a guaranteed minimum delay between subsequent requests.
|
||||
*
|
||||
* @author afriese - initial contribution
|
||||
*/
|
||||
private class WebRequestExecutor implements Runnable {
|
||||
|
||||
/**
|
||||
* queue which holds the commands to execute
|
||||
*/
|
||||
private final Queue<MyUplinkCommand> commandQueue;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*/
|
||||
WebRequestExecutor() {
|
||||
this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
|
||||
}
|
||||
|
||||
private void processAuthenticationResult(CommunicationStatus status, JsonObject jsonObject) {
|
||||
String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR);
|
||||
if (msg == null || msg.isBlank()) {
|
||||
msg = status.getMessage();
|
||||
}
|
||||
|
||||
switch (status.getHttpCode()) {
|
||||
case BAD_REQUEST:
|
||||
bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
msg);
|
||||
setAuthenticated(false);
|
||||
break;
|
||||
case OK:
|
||||
String accessToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_ACCESS_TOKEN);
|
||||
int expiresInSeconds = Utils.getAsInt(jsonObject, JSON_KEY_AUTH_EXPIRES_IN);
|
||||
if (accessToken != null && expiresInSeconds != 0) {
|
||||
WebInterface.this.accessToken = accessToken;
|
||||
tokenRefreshDate = Instant.now();
|
||||
tokenExpiry = tokenRefreshDate.plusSeconds(expiresInSeconds);
|
||||
|
||||
logger.debug("access token refreshed: {}, expiry: {}", tokenRefreshDate.toString(),
|
||||
tokenExpiry.toString());
|
||||
|
||||
bridgeStatusHandler.updateStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE,
|
||||
STATUS_TOKEN_VALIDATED);
|
||||
setAuthenticated(true);
|
||||
handler.startDiscovery();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
msg);
|
||||
setAuthenticated(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* puts a command into the queue
|
||||
*
|
||||
* @param command
|
||||
*/
|
||||
void enqueue(MyUplinkCommand command) {
|
||||
try {
|
||||
commandQueue.add(command);
|
||||
} catch (IllegalStateException ex) {
|
||||
if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
|
||||
logger.debug(
|
||||
"Could not add command to command queue because queue is already full. Maybe NIBE myUplink is down?");
|
||||
} else {
|
||||
logger.warn("Could not add command to queue - IllegalStateException");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* executes the web request
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
logger.debug("run queued commands, queue size is {}", commandQueue.size());
|
||||
if (!isAuthenticated()) {
|
||||
authenticate();
|
||||
} else {
|
||||
refreshAccessToken();
|
||||
|
||||
if (isAuthenticated() && !commandQueue.isEmpty()) {
|
||||
try {
|
||||
executeCommand();
|
||||
} catch (Exception ex) {
|
||||
logger.warn("command execution ended with exception:", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* authenticates with the NIBE myUplink Cloud interface.
|
||||
*/
|
||||
private synchronized void authenticate() {
|
||||
setAuthenticated(false);
|
||||
MyUplinkCommand loginCommand = new Login(handler, this::processAuthenticationResult);
|
||||
try {
|
||||
loginCommand.performAction(httpClient, "");
|
||||
} catch (ValidationException e) {
|
||||
// this cannot happen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* periodically refreshed the access token.
|
||||
*/
|
||||
private synchronized void refreshAccessToken() {
|
||||
Instant now = Instant.now();
|
||||
|
||||
if (now.plus(WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES, ChronoUnit.MINUTES).isAfter(tokenExpiry)
|
||||
|| now.isAfter(tokenRefreshDate.plus(WEB_REQUEST_TOKEN_MAX_AGE_MINUTES, ChronoUnit.MINUTES))) {
|
||||
logger.debug("access token needs to be refreshed, last refresh: {}, expiry: {}",
|
||||
tokenRefreshDate.toString(), tokenExpiry.toString());
|
||||
|
||||
MyUplinkCommand refreshCommand = new Login(handler, this::processAuthenticationResult);
|
||||
try {
|
||||
refreshCommand.performAction(httpClient, "");
|
||||
} catch (ValidationException e) {
|
||||
// this cannot happen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* executes the next command in the queue. requires authenticated session.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private void executeCommand() throws ValidationException {
|
||||
MyUplinkCommand command = commandQueue.poll();
|
||||
if (command != null) {
|
||||
command.performAction(httpClient, accessToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to set up interface
|
||||
*/
|
||||
public WebInterface(ScheduledExecutorService scheduler, MyUplinkBridgeHandler handler, HttpClient httpClient,
|
||||
StatusHandler bridgeStatusHandler) {
|
||||
this.handler = handler;
|
||||
this.bridgeStatusHandler = bridgeStatusHandler;
|
||||
this.scheduler = scheduler;
|
||||
this.httpClient = httpClient;
|
||||
this.tokenExpiry = OUTDATED_DATE;
|
||||
this.tokenRefreshDate = OUTDATED_DATE;
|
||||
this.accessToken = "";
|
||||
this.requestExecutor = new WebRequestExecutor();
|
||||
this.requestExecutorJobReference = new AtomicReference<>(null);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
setAuthenticated(false);
|
||||
updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
|
||||
WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* queues any command for execution
|
||||
*
|
||||
* @param command
|
||||
*/
|
||||
public void enqueueCommand(MyUplinkCommand command) {
|
||||
requestExecutor.enqueue(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* will be called by the ThingHandler to abort periodic jobs.
|
||||
*/
|
||||
public void dispose() {
|
||||
logger.debug("Webinterface disposed.");
|
||||
cancelJobReference(requestExecutorJobReference);
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns authentication status.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private boolean isAuthenticated() {
|
||||
return authenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* update the authentication status, also resets token data.
|
||||
*
|
||||
* @param authenticated
|
||||
*/
|
||||
private void setAuthenticated(boolean authenticated) {
|
||||
this.authenticated = authenticated;
|
||||
if (!authenticated) {
|
||||
this.tokenExpiry = OUTDATED_DATE;
|
||||
this.accessToken = "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the current access token.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.myuplink.internal.MyUplinkBindingConstants;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.command.account.GetSystems;
|
||||
import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkAccountHandler;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* this class will handle discovery of wallboxes and circuits within the site configured.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MyUplinkDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MyUplinkDiscoveryService.class);
|
||||
private @NonNullByDefault({}) MyUplinkAccountHandler bridgeHandler;
|
||||
|
||||
public MyUplinkDiscoveryService() throws IllegalArgumentException {
|
||||
super(Set.of(MyUplinkBindingConstants.THING_TYPE_ACCOUNT), 300, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(ThingHandler handler) {
|
||||
if (handler instanceof MyUplinkAccountHandler accountHandler) {
|
||||
this.bridgeHandler = accountHandler;
|
||||
this.bridgeHandler.setDiscoveryService(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return bridgeHandler;
|
||||
}
|
||||
|
||||
// method is defined in both implemented interface and inherited class, thus we must define a behaviour here.
|
||||
@Override
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
bridgeHandler.enqueueCommand(new GetSystems(bridgeHandler, this::processMyUplinkDiscoveryResult));
|
||||
}
|
||||
|
||||
/**
|
||||
* callback that handles json result data to provide discovery result.
|
||||
*
|
||||
* @param site
|
||||
*/
|
||||
void processMyUplinkDiscoveryResult(CommunicationStatus status, JsonObject json) {
|
||||
logger.debug("processMyUplinkDiscoveryResult {}", json);
|
||||
|
||||
JsonArray systems = json.getAsJsonArray(JSON_KEY_SYSTEMS);
|
||||
if (systems == null || systems.isEmpty()) {
|
||||
logger.debug("System discovery finished, no systems found.");
|
||||
} else {
|
||||
systems.forEach(this::handleSystemDiscovery);
|
||||
}
|
||||
}
|
||||
|
||||
void handleSystemDiscovery(JsonElement json) {
|
||||
logger.debug("handleSystemDiscovery {}", json);
|
||||
|
||||
JsonObject system = json.getAsJsonObject();
|
||||
String systemId = Utils.getAsString(system, JSON_KEY_SYSTEM_ID);
|
||||
JsonArray devices = system.getAsJsonArray(JSON_KEY_DEVICES);
|
||||
if (devices == null || devices.isEmpty()) {
|
||||
logger.debug("System discovery finished, no devices found.");
|
||||
} else {
|
||||
devices.forEach(device -> handleDeviceDiscovery(device, systemId));
|
||||
}
|
||||
}
|
||||
|
||||
void handleDeviceDiscovery(JsonElement json, @Nullable String systemId) {
|
||||
logger.debug("handleDeviceDiscovery {}", json);
|
||||
|
||||
JsonObject device = json.getAsJsonObject();
|
||||
String id = Utils.getAsString(device, JSON_KEY_GENERIC_ID);
|
||||
String serial = Utils.getAsString(device.getAsJsonObject(JSON_KEY_PRODUCT), JSON_KEY_SERIAL);
|
||||
String name = Utils.getAsString(device.getAsJsonObject(JSON_KEY_PRODUCT), JSON_KEY_NAME);
|
||||
|
||||
if (id != null && serial != null) {
|
||||
DiscoveryResultBuilder builder;
|
||||
builder = initDiscoveryResultBuilder(DEVICE_GENERIC_DEVICE, id, name);
|
||||
builder.withProperty(THING_CONFIG_SERIAL, serial);
|
||||
if (systemId != null) {
|
||||
builder.withProperty(THING_CONFIG_SYSTEM_ID, systemId);
|
||||
}
|
||||
thingDiscovered(builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sends discovery notification to the framework.
|
||||
*
|
||||
* @param deviceType
|
||||
* @param deviceId
|
||||
* @param deviceName
|
||||
*/
|
||||
DiscoveryResultBuilder initDiscoveryResultBuilder(String deviceType, String deviceId, @Nullable String deviceName) {
|
||||
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
|
||||
ThingTypeUID typeUid = new ThingTypeUID(BINDING_ID, deviceType);
|
||||
|
||||
ThingUID thingUID = new ThingUID(typeUid, bridgeUID, deviceId);
|
||||
String label = deviceName != null ? deviceName : deviceId;
|
||||
|
||||
return DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID).withLabel(label)
|
||||
.withProperty(THING_CONFIG_ID, deviceId).withRepresentationProperty(THING_CONFIG_ID);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Channel;
|
||||
|
||||
/**
|
||||
* this interface provides all methods which deal with channels
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ChannelProvider {
|
||||
|
||||
/**
|
||||
* returns the channel with given channelId and groupId. If no channel matches, null is returned.
|
||||
*
|
||||
* @param groupId
|
||||
* group ID of the channel
|
||||
* @param channelId
|
||||
* channel ID of the channel
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
Channel getChannel(String groupId, String channelId);
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.myuplink.internal.provider.ChannelFactory;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
/**
|
||||
* this interface provides all methods which deal with channels
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface DynamicChannelProvider {
|
||||
|
||||
/**
|
||||
* registers a channel with the thing.
|
||||
*
|
||||
* @param channel
|
||||
* the channel to be registered.
|
||||
*/
|
||||
void registerChannel(Channel channel);
|
||||
|
||||
/**
|
||||
* Simple Getter to retrieve the Channelfactory of this thing.
|
||||
*
|
||||
* @return
|
||||
* the ChannelFactory
|
||||
*/
|
||||
ChannelFactory getChannelFactory();
|
||||
|
||||
/**
|
||||
* Simple Getter to retrieve the ThingUid
|
||||
*
|
||||
* @return
|
||||
* the ThingUid
|
||||
*/
|
||||
ThingUID getThingUid();
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.handler;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.myuplink.internal.AtomicReferenceTrait;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.account.GetSystems;
|
||||
import org.openhab.binding.myuplink.internal.config.MyUplinkConfiguration;
|
||||
import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
|
||||
import org.openhab.binding.myuplink.internal.connector.WebInterface;
|
||||
import org.openhab.binding.myuplink.internal.discovery.MyUplinkDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The {@link myUplinkHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Alexander Friese - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MyUplinkAccountHandler extends BaseBridgeHandler implements MyUplinkBridgeHandler, AtomicReferenceTrait {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MyUplinkAccountHandler.class);
|
||||
|
||||
/**
|
||||
* Schedule for polling live data
|
||||
*/
|
||||
private final AtomicReference<@Nullable Future<?>> dataPollingJobReference;
|
||||
|
||||
private @Nullable DiscoveryService discoveryService;
|
||||
|
||||
/**
|
||||
* Interface object for querying the NIBE myUplink API.
|
||||
*/
|
||||
private WebInterface webInterface;
|
||||
|
||||
public MyUplinkAccountHandler(Bridge bridge, HttpClient httpClient) {
|
||||
super(bridge);
|
||||
this.dataPollingJobReference = new AtomicReference<>(null);
|
||||
this.webInterface = new WebInterface(scheduler, this, httpClient, super::updateStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("About to initialize myUplink Account");
|
||||
MyUplinkConfiguration config = getBridgeConfiguration();
|
||||
logger.debug("myUplink Account initialized with configuration: {}", config.toString());
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, STATUS_WAITING_FOR_LOGIN);
|
||||
|
||||
if (config.getClientId().isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, STATUS_CONFIG_ERROR_NO_CLIENT_ID);
|
||||
} else if (config.getClientSecret().isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
STATUS_CONFIG_ERROR_NO_CLIENT_SECRET);
|
||||
} else {
|
||||
webInterface.start();
|
||||
startPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the polling.
|
||||
*/
|
||||
private void startPolling() {
|
||||
updateJobReference(dataPollingJobReference, scheduler.scheduleWithFixedDelay(this::pollingRun,
|
||||
POLLING_INITIAL_DELAY, getBridgeConfiguration().getDataPollingInterval(), TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the Easee Cloud API one time.
|
||||
*/
|
||||
void pollingRun() {
|
||||
GetSystems state = new GetSystems(this, this::updateOnlineStatus);
|
||||
enqueueCommand(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* result processor to handle online status updates
|
||||
*
|
||||
* @param status of command execution
|
||||
* @param jsonObject json respone result
|
||||
*/
|
||||
protected final void updateOnlineStatus(CommunicationStatus status, JsonObject jsonObject) {
|
||||
String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR);
|
||||
if (msg == null || msg.isBlank()) {
|
||||
msg = status.getMessage();
|
||||
}
|
||||
|
||||
switch (status.getHttpCode()) {
|
||||
case OK:
|
||||
case ACCEPTED:
|
||||
super.updateStatus(ThingStatus.ONLINE);
|
||||
break;
|
||||
default:
|
||||
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the bridge.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Handler disposing");
|
||||
cancelJobReference(dataPollingJobReference);
|
||||
webInterface.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MyUplinkConfiguration getBridgeConfiguration() {
|
||||
return this.getConfigAs(MyUplinkConfiguration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(MyUplinkDiscoveryService.class);
|
||||
}
|
||||
|
||||
public void setDiscoveryService(MyUplinkDiscoveryService discoveryService) {
|
||||
this.discoveryService = discoveryService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDiscovery() {
|
||||
DiscoveryService discoveryService = this.discoveryService;
|
||||
if (discoveryService != null) {
|
||||
discoveryService.startScan(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enqueueCommand(MyUplinkCommand command) {
|
||||
webInterface.enqueueCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.binding.BridgeHandler;
|
||||
|
||||
/**
|
||||
* public interface of the {@link MyUplinkBridgeHandler}
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MyUplinkBridgeHandler extends BridgeHandler, MyUplinkThingHandler {
|
||||
|
||||
/**
|
||||
* starts discovery of Nibe devices
|
||||
*/
|
||||
void startDiscovery();
|
||||
}
|
@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.handler;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.myuplink.internal.AtomicReferenceTrait;
|
||||
import org.openhab.binding.myuplink.internal.MyUplinkBindingConstants;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
|
||||
import org.openhab.binding.myuplink.internal.command.account.GetSystems;
|
||||
import org.openhab.binding.myuplink.internal.command.device.GetPoints;
|
||||
import org.openhab.binding.myuplink.internal.command.device.GetSmartHomeMode;
|
||||
import org.openhab.binding.myuplink.internal.command.device.SetPoints;
|
||||
import org.openhab.binding.myuplink.internal.command.device.SetPointsAdvanced;
|
||||
import org.openhab.binding.myuplink.internal.command.device.SetSmartHomeMode;
|
||||
import org.openhab.binding.myuplink.internal.config.MyUplinkConfiguration;
|
||||
import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
|
||||
import org.openhab.binding.myuplink.internal.provider.ChannelFactory;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The {@link MyUplinkGenericDeviceHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MyUplinkGenericDeviceHandler extends BaseThingHandler
|
||||
implements MyUplinkThingHandler, DynamicChannelProvider, AtomicReferenceTrait {
|
||||
private final Logger logger = LoggerFactory.getLogger(MyUplinkGenericDeviceHandler.class);
|
||||
|
||||
/**
|
||||
* Schedule for polling live data
|
||||
*/
|
||||
private final AtomicReference<@Nullable Future<?>> dataPollingJobReference;
|
||||
|
||||
private final ChannelFactory channelFactory;
|
||||
|
||||
private final Configuration config;
|
||||
|
||||
public MyUplinkGenericDeviceHandler(Thing thing, ChannelFactory channelFactory) {
|
||||
super(thing);
|
||||
this.dataPollingJobReference = new AtomicReference<>(null);
|
||||
this.channelFactory = channelFactory;
|
||||
this.config = getConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("About to initialize myUplink Generic Device with id: {}", getDeviceId());
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, STATUS_WAITING_FOR_BRIDGE);
|
||||
startPolling();
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return config.get(THING_CONFIG_ID).toString();
|
||||
}
|
||||
|
||||
private void updatePropertiesAndOnlineStatus(CommunicationStatus status, JsonObject systemsJson) {
|
||||
JsonObject deviceFound = extractDevice(systemsJson);
|
||||
|
||||
if (deviceFound != null) {
|
||||
Map<String, String> properties = editProperties();
|
||||
String currentFwVersion = Utils.getAsString(deviceFound, JSON_KEY_CURRENT_FW_VERSION, GENERIC_NO_VAL);
|
||||
properties.put(THING_CONFIG_CURRENT_FW_VERSION, currentFwVersion);
|
||||
updateProperties(properties);
|
||||
|
||||
String connectionStatus = Utils.getAsString(deviceFound, JSON_KEY_CONNECTION_STATE, GENERIC_NO_VAL);
|
||||
if (connectionStatus.equals(JSON_VAL_CONNECTION_CONNECTED)) {
|
||||
super.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
|
||||
} else {
|
||||
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, STATUS_NO_CONNECTION);
|
||||
}
|
||||
} else {
|
||||
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, STATUS_DEVICE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
private final @Nullable JsonObject extractDevice(JsonObject systemsJson) {
|
||||
JsonArray systems = systemsJson.getAsJsonArray(JSON_KEY_SYSTEMS);
|
||||
if (systems != null && !systems.isEmpty()) {
|
||||
for (JsonElement systemJson : systems) {
|
||||
JsonObject system = systemJson.getAsJsonObject();
|
||||
JsonArray devices = system.getAsJsonArray(JSON_KEY_DEVICES);
|
||||
if (devices != null && !devices.isEmpty()) {
|
||||
for (JsonElement deviceJson : devices) {
|
||||
JsonObject device = deviceJson.getAsJsonObject();
|
||||
String deviceId = Utils.getAsString(device, JSON_KEY_GENERIC_ID, GENERIC_NO_VAL);
|
||||
if (deviceId.equals(getDeviceId())) {
|
||||
return device;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MyUplinkCommand buildMyUplinkCommand(Command command, Channel channel) {
|
||||
var deviceId = config.get(MyUplinkBindingConstants.THING_CONFIG_ID).toString();
|
||||
String systemId = "";
|
||||
if (config.containsKey(THING_CONFIG_SYSTEM_ID)) {
|
||||
systemId = config.get(THING_CONFIG_SYSTEM_ID).toString();
|
||||
}
|
||||
|
||||
var channelTypeId = Utils.getChannelTypeId(channel);
|
||||
return switch (channelTypeId) {
|
||||
case CHANNEL_TYPE_RW_COMMAND ->
|
||||
new SetPointsAdvanced(this, channel, command, deviceId, this::updateOnlineStatus);
|
||||
case CHANNEL_TYPE_RW_MODE -> {
|
||||
if (systemId.isBlank()) {
|
||||
throw new UnsupportedOperationException("systemId not configured");
|
||||
}
|
||||
yield new SetSmartHomeMode(this, channel, command, systemId, this::updateOnlineStatus);
|
||||
}
|
||||
default -> new SetPoints(this, channel, command, deviceId, this::updateOnlineStatus);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the polling.
|
||||
*/
|
||||
private void startPolling() {
|
||||
updateJobReference(dataPollingJobReference, scheduler.scheduleWithFixedDelay(this::pollingRun,
|
||||
POLLING_INITIAL_DELAY, getBridgeConfiguration().getDataPollingInterval(), TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the myUplink Cloud API one time.
|
||||
*/
|
||||
void pollingRun() {
|
||||
String deviceId = config.get(THING_CONFIG_ID).toString();
|
||||
String systemId = "";
|
||||
if (config.containsKey(THING_CONFIG_SYSTEM_ID)) {
|
||||
systemId = config.get(THING_CONFIG_SYSTEM_ID).toString();
|
||||
}
|
||||
logger.debug("polling device data for {}", deviceId);
|
||||
|
||||
// proceed if device is online
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
enqueueCommand(new GetPoints(this, deviceId, this::updateOnlineStatus));
|
||||
if (!systemId.isBlank()) {
|
||||
enqueueCommand(new GetSmartHomeMode(this, systemId, this::updateOnlineStatus));
|
||||
}
|
||||
}
|
||||
enqueueCommand(new GetSystems(this, this::updatePropertiesAndOnlineStatus));
|
||||
}
|
||||
|
||||
/**
|
||||
* result processor to handle online status updates
|
||||
*
|
||||
* @param status of command execution
|
||||
* @param jsonObject json respone result
|
||||
*/
|
||||
protected final void updateOnlineStatus(CommunicationStatus status, JsonObject jsonObject) {
|
||||
String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR);
|
||||
if (msg == null || msg.isBlank()) {
|
||||
msg = status.getMessage();
|
||||
}
|
||||
|
||||
switch (status.getHttpCode()) {
|
||||
case OK:
|
||||
case ACCEPTED:
|
||||
super.updateStatus(ThingStatus.ONLINE);
|
||||
break;
|
||||
case BAD_REQUEST:
|
||||
case UNAUTHORIZED:
|
||||
case FORBIDDEN:
|
||||
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
|
||||
break;
|
||||
default:
|
||||
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the thing.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Handler disposing.");
|
||||
cancelJobReference(dataPollingJobReference);
|
||||
}
|
||||
|
||||
/**
|
||||
* adds a channel.
|
||||
*/
|
||||
@Override
|
||||
public void registerChannel(Channel channel) {
|
||||
ThingBuilder thingBuilder = editThing();
|
||||
thingBuilder.withChannel(channel);
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* will update all channels provided in the map
|
||||
*/
|
||||
@Override
|
||||
public void updateChannelStatus(Map<Channel, State> values) {
|
||||
logger.debug("Handling heatpump channel update.");
|
||||
|
||||
for (Channel channel : values.keySet()) {
|
||||
if (getThing().getChannels().contains(channel)) {
|
||||
if (isLinked(channel.getUID())) {
|
||||
State value = values.get(channel);
|
||||
if (value != null) {
|
||||
logger.debug("Channel is to be updated: {}: {}", channel.getUID().getAsString(), value);
|
||||
updateState(channel.getUID(), value);
|
||||
} else {
|
||||
logger.debug("Value is null or not provided by myUplink Cloud (channel: {})",
|
||||
channel.getUID().getAsString());
|
||||
updateState(channel.getUID(), UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("Could not identify channel: {} for model {}", channel.getUID().getAsString(),
|
||||
getThing().getThingTypeUID().getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enqueueCommand(MyUplinkCommand command) {
|
||||
MyUplinkBridgeHandler bridgeHandler = getBridgeHandler();
|
||||
if (bridgeHandler != null) {
|
||||
bridgeHandler.enqueueCommand(command);
|
||||
} else {
|
||||
// this should not happen
|
||||
logger.warn("no bridge handler found");
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable MyUplinkBridgeHandler getBridgeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
return bridge != null && bridge.getHandler() instanceof MyUplinkBridgeHandler handler ? handler : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MyUplinkConfiguration getBridgeConfiguration() {
|
||||
MyUplinkBridgeHandler bridgeHandler = getBridgeHandler();
|
||||
return bridgeHandler == null ? new MyUplinkConfiguration() : bridgeHandler.getBridgeConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThingUID getThingUid() {
|
||||
return getThing().getUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFactory getChannelFactory() {
|
||||
return channelFactory;
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.handler;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
|
||||
import org.openhab.binding.myuplink.internal.config.MyUplinkConfiguration;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelGroupUID;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/**
|
||||
* public interface of the {@link MyUplinkThingHandler}. provides some default implementations which can be used by both
|
||||
* BridgeHandlers and ThingHandlers.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MyUplinkThingHandler extends ThingHandler, ChannelProvider {
|
||||
|
||||
/**
|
||||
* just to avoid usage of static loggers.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Logger getLogger();
|
||||
|
||||
/**
|
||||
* method which updates the channels. needs to be implemented by thing types which have channels.
|
||||
*
|
||||
* @param values key-value list where key is the channel
|
||||
*/
|
||||
default void updateChannelStatus(Map<Channel, State> values) {
|
||||
getLogger().debug("updateChannelStatus not implemented/supported by this thing type");
|
||||
}
|
||||
|
||||
/**
|
||||
* return the bridge's configuration
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
MyUplinkConfiguration getBridgeConfiguration();
|
||||
|
||||
/**
|
||||
* passes a new command o the command queue
|
||||
*
|
||||
* @param command to be queued/executed
|
||||
*/
|
||||
void enqueueCommand(MyUplinkCommand command);
|
||||
|
||||
/**
|
||||
* default implementation to handle commands
|
||||
*
|
||||
* @param channelUID
|
||||
* @param command
|
||||
*/
|
||||
@Override
|
||||
default void handleCommand(ChannelUID channelUID, Command command) {
|
||||
getLogger().debug("command for {}: {}", channelUID, command);
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
return;
|
||||
}
|
||||
|
||||
String group = channelUID.getGroupId();
|
||||
group = group == null ? "" : group;
|
||||
Channel channel = getChannel(group, channelUID.getIdWithoutGroup());
|
||||
if (channel == null) {
|
||||
// this should not happen
|
||||
getLogger().warn("channel not found: {}", channelUID);
|
||||
return;
|
||||
}
|
||||
|
||||
String channelType = Utils.getChannelTypeId(channel);
|
||||
if (!channelType.startsWith(CHANNEL_TYPE_PREFIX_RW)) {
|
||||
getLogger().warn("channel '{}', type '{}' does not support write access - value to set '{}'",
|
||||
channelUID.getIdWithoutGroup(), channelType, command);
|
||||
throw new UnsupportedOperationException(
|
||||
"channel (" + channelUID.getIdWithoutGroup() + ") does not support write access");
|
||||
}
|
||||
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
getLogger().debug("Thing is not online, thus no commands will be handled");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
enqueueCommand(buildMyUplinkCommand(command, channel));
|
||||
} catch (UnsupportedOperationException e) {
|
||||
getLogger().warn("Unsupported command: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* builds the MyUplinkCommand which can be send to the webinterface.
|
||||
*
|
||||
* @param command the openhab raw command received from the framework
|
||||
* @param channel the channel which belongs to the command.
|
||||
* @return
|
||||
*/
|
||||
default MyUplinkCommand buildMyUplinkCommand(Command command, Channel channel) {
|
||||
throw new UnsupportedOperationException("buildMyUplinkCommand not implemented/supported by this thing type");
|
||||
}
|
||||
|
||||
/**
|
||||
* determines the channel for a given groupId and channelId.
|
||||
*
|
||||
* @param groupId groupId of the channel
|
||||
* @param channelId channelId of the channel
|
||||
*/
|
||||
@Override
|
||||
default @Nullable Channel getChannel(String groupId, String channelId) {
|
||||
ThingUID thingUID = this.getThing().getUID();
|
||||
ChannelUID channelUID;
|
||||
if (!groupId.isEmpty()) {
|
||||
ChannelGroupUID channelGroupUID = new ChannelGroupUID(thingUID, groupId);
|
||||
channelUID = new ChannelUID(channelGroupUID, channelId);
|
||||
} else {
|
||||
channelUID = new ChannelUID(thingUID, channelId);
|
||||
}
|
||||
return getThing().getChannel(channelUID);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
|
||||
/**
|
||||
* functional interface to provide a function to update status of a thing or bridge.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@NonNullByDefault
|
||||
public interface StatusHandler {
|
||||
/**
|
||||
* Called from WebInterface#authenticate() to update
|
||||
* the thing status because updateStatus is protected.
|
||||
*
|
||||
* @param status Thing status
|
||||
* @param statusDetail Thing status detail
|
||||
* @param description Thing status description
|
||||
*/
|
||||
void updateStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, String description);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* exception whichs is used to state a validation error
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConfigurationException extends RuntimeException {
|
||||
private static final long serialVersionUID = 5736865225953884234L;
|
||||
|
||||
public ConfigurationException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ConfigurationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ConfigurationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.model;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.handler.ChannelProvider;
|
||||
import org.openhab.binding.myuplink.internal.handler.DynamicChannelProvider;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* transforms the http response into the openhab datamodel (instances of State)
|
||||
* this is a generic trnasformer which tries to map json fields 1:1 to channels.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GenericResponseTransformer implements ResponseTransformer {
|
||||
private final Logger logger = LoggerFactory.getLogger(GenericResponseTransformer.class);
|
||||
private final ChannelProvider channelProvider;
|
||||
private final @Nullable DynamicChannelProvider dynamicChannelProvider;
|
||||
|
||||
public GenericResponseTransformer(ChannelProvider channelProvider) {
|
||||
this.channelProvider = channelProvider;
|
||||
this.dynamicChannelProvider = channelProvider instanceof DynamicChannelProvider
|
||||
? (DynamicChannelProvider) channelProvider
|
||||
: null;
|
||||
}
|
||||
|
||||
public Map<Channel, State> transform(JsonObject jsonData, String group) {
|
||||
Map<Channel, State> result = new HashMap<>(20);
|
||||
|
||||
for (JsonElement channelData : Utils.getAsJsonArray(jsonData, JSON_KEY_ROOT_DATA)) {
|
||||
|
||||
logger.debug("received channel data: {}", channelData.toString());
|
||||
|
||||
var value = Utils.getAsBigDecimal(channelData.getAsJsonObject(), JSON_KEY_CHANNEL_VALUE);
|
||||
var unit = UnitUtils.parseUnit(
|
||||
Utils.fixUnit(Utils.getAsString(channelData.getAsJsonObject(), JSON_KEY_CHANNEL_UNIT, "")));
|
||||
var channelId = Utils.getAsString(channelData.getAsJsonObject(), JSON_KEY_CHANNEL_ID, GENERIC_NO_VAL);
|
||||
|
||||
Channel channel;
|
||||
var dcp = dynamicChannelProvider;
|
||||
if (dcp != null) {
|
||||
channel = getOrCreateChannel(dcp, channelId, channelData.getAsJsonObject());
|
||||
} else {
|
||||
channel = channelProvider.getChannel(group, channelId);
|
||||
}
|
||||
|
||||
if (channel == null) {
|
||||
logger.debug("Channel not found: {}#{}, dynamic channels not support by thing.", group, channelId);
|
||||
} else {
|
||||
logger.debug("mapping value '{}' to channel {}", value, channel.getUID().getId());
|
||||
|
||||
if (value == null) {
|
||||
result.put(channel, UnDefType.NULL);
|
||||
} else {
|
||||
try {
|
||||
var channelTypeId = Utils.getChannelTypeId(channel);
|
||||
State newState;
|
||||
if (channelTypeId.equals(CHANNEL_TYPE_RW_SWITCH)) {
|
||||
newState = convertToOnOffType(value.stripTrailingZeros().toString());
|
||||
} else if (channelTypeId.equals(CHANNEL_TYPE_RW_COMMAND)) {
|
||||
newState = new StringType(value.toString());
|
||||
} else if (unit != null) {
|
||||
newState = new QuantityType<>(value, unit);
|
||||
} else {
|
||||
newState = new DecimalType(value.stripTrailingZeros());
|
||||
}
|
||||
|
||||
if (newState == UnDefType.NULL) {
|
||||
logger.warn("no mapping implemented for channel type '{}'", channelTypeId);
|
||||
} else {
|
||||
result.put(channel, newState);
|
||||
}
|
||||
} catch (NumberFormatException | DateTimeParseException ex) {
|
||||
logger.warn("caught exception while parsing data for channel {} (value '{}'). Exception: {}",
|
||||
channel.getUID().getId(), value, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Channel getOrCreateChannel(DynamicChannelProvider dcp, String channelId, JsonObject channelData) {
|
||||
var result = channelProvider.getChannel(EMPTY, channelId);
|
||||
if (result == null) {
|
||||
result = dcp.getChannelFactory().createChannel(dcp.getThingUid(), channelData);
|
||||
dcp.registerChannel(result);
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private OnOffType convertToOnOffType(String value) {
|
||||
return switch (value) {
|
||||
case "1" -> OnOffType.ON;
|
||||
case "1.0" -> OnOffType.ON;
|
||||
default -> OnOffType.OFF;
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* transforms the http response into the openhab datamodel (instances of State)
|
||||
* this is an interface which can be implemented by different transformer classes
|
||||
*
|
||||
* @author Anders Alfredsson - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ResponseTransformer {
|
||||
|
||||
/**
|
||||
* Transform the received data into a Map of channels and the State they should be updated to
|
||||
*
|
||||
* @param jsonData The input json data
|
||||
* @param group The channel group
|
||||
* @return
|
||||
*/
|
||||
Map<Channel, State> transform(JsonObject jsonData, String group);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.model;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.binding.myuplink.internal.handler.ChannelProvider;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* transforms the http response into the openhab datamodel (instances of State)
|
||||
* this is a transformer for the smart home mode data received from the api.
|
||||
*
|
||||
* @author Anders Alfredsson - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartHomeModeResponseTransformer implements ResponseTransformer {
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartHomeModeResponseTransformer.class);
|
||||
private final ChannelProvider channelProvider;
|
||||
|
||||
public SmartHomeModeResponseTransformer(ChannelProvider channelProvider) {
|
||||
this.channelProvider = channelProvider;
|
||||
}
|
||||
|
||||
public Map<Channel, State> transform(JsonObject jsonData, String group) {
|
||||
String mode = Utils.getAsString(jsonData, JSON_KEY_SMART_HOME_MODE);
|
||||
Channel channel = channelProvider.getChannel(group, CHANNEL_ID_SMART_HOME_MODE);
|
||||
|
||||
if (channel == null) {
|
||||
logger.warn(
|
||||
"Smart home mode channel not found. This is likely because of a bug. Please report to the developers.");
|
||||
return Map.of();
|
||||
} else {
|
||||
State newState;
|
||||
|
||||
if (mode == null) {
|
||||
newState = UnDefType.UNDEF;
|
||||
} else {
|
||||
newState = new StringType(mode);
|
||||
}
|
||||
|
||||
return Map.of(channel, newState);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* exception whichs is used to state a validation error
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ValidationException extends Exception {
|
||||
private static final long serialVersionUID = -6479556472780307224L;
|
||||
|
||||
public ValidationException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ValidationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ValidationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.provider;
|
||||
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.openhab.core.util.StringUtils;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Factory that contains logic to create dynamic channels.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@Component(service = { ChannelFactory.class })
|
||||
@NonNullByDefault
|
||||
public class ChannelFactory {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChannelFactory.class);
|
||||
|
||||
private final MyUplinkChannelTypeProvider channelTypeProvider;
|
||||
private final ChannelTypeRegistry channelTypeRegistry;
|
||||
|
||||
@Activate
|
||||
public ChannelFactory(@Reference MyUplinkChannelTypeProvider channelTypeProvider,
|
||||
@Reference ChannelTypeRegistry channelTypeRegistry) {
|
||||
this.channelTypeProvider = channelTypeProvider;
|
||||
this.channelTypeRegistry = channelTypeRegistry;
|
||||
}
|
||||
|
||||
public Channel createChannel(ThingUID thingUID, JsonObject channelData) {
|
||||
final var channelId = Utils.getAsString(channelData, JSON_KEY_CHANNEL_ID, GENERIC_NO_VAL);
|
||||
final var label = Utils.getAsString(channelData, JSON_KEY_CHANNEL_LABEL, GENERIC_NO_VAL);
|
||||
final var unit = Utils.fixUnit(Utils.getAsString(channelData, JSON_KEY_CHANNEL_UNIT, ""));
|
||||
final var strVal = Utils.getAsString(channelData, JSON_KEY_CHANNEL_STR_VAL, GENERIC_NO_VAL);
|
||||
final var writable = Utils.getAsBool(channelData, JSON_KEY_CHANNEL_WRITABLE, Boolean.FALSE);
|
||||
final var enumValues = Utils.getAsJsonArray(channelData, JSON_KEY_CHANNEL_ENUM_VALUES);
|
||||
final var minValue = Utils.getAsBigDecimal(channelData, JSON_KEY_CHANNEL_MIN);
|
||||
final var maxValue = Utils.getAsBigDecimal(channelData, JSON_KEY_CHANNEL_MAX);
|
||||
final var stepValue = Utils.getAsBigDecimal(channelData, JSON_KEY_CHANNEL_STEP);
|
||||
|
||||
ChannelTypeUID channelTypeUID = null;
|
||||
if (enumValues.isEmpty()) {
|
||||
if (!writable) {
|
||||
channelTypeUID = determineStaticChannelTypeUID(unit, strVal.contains(JSON_VAL_DECIMAL_SEPARATOR));
|
||||
} else {
|
||||
channelTypeUID = getOrBuildNumberChannelType(channelId, unit, minValue, maxValue, stepValue);
|
||||
}
|
||||
} else {
|
||||
channelTypeUID = determineEnumChannelTypeUID(channelId, enumValues, writable);
|
||||
}
|
||||
|
||||
final var channelUID = new ChannelUID(thingUID, channelId);
|
||||
final var acceptedType = determineAcceptedType(channelTypeUID, unit);
|
||||
final var builder = ChannelBuilder.create(channelUID).withLabel(label).withDescription(label)
|
||||
.withType(channelTypeUID).withAcceptedItemType(acceptedType);
|
||||
|
||||
if (writable) {
|
||||
var props = new HashMap<String, String>();
|
||||
props.put(PARAMETER_NAME_VALIDATION_REGEXP, DEFAULT_VALIDATION_EXPRESSION);
|
||||
builder.withProperties(props);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
String determineAcceptedType(ChannelTypeUID channelTypeUID, String unit) {
|
||||
if (channelTypeUID.getId().equals(CHANNEL_TYPE_RW_SWITCH)) {
|
||||
return CoreItemFactory.SWITCH;
|
||||
} else if (unit.isEmpty()) {
|
||||
return CoreItemFactory.NUMBER;
|
||||
} else {
|
||||
Unit<?> parsedUnit = UnitUtils.parseUnit(unit);
|
||||
String dimension = parsedUnit == null ? null : UnitUtils.getDimensionName(parsedUnit);
|
||||
|
||||
if (dimension == null || dimension.isEmpty()) {
|
||||
logger.warn("Could not parse unit: '{}'", unit);
|
||||
return CoreItemFactory.NUMBER;
|
||||
} else {
|
||||
return CoreItemFactory.NUMBER + ":" + dimension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelTypeUID determineStaticChannelTypeUID(String unit, boolean isDouble) {
|
||||
String typeName = switch (unit) {
|
||||
case CHANNEL_TYPE_ENERGY_UNIT -> CHANNEL_TYPE_ENERGY;
|
||||
case CHANNEL_TYPE_PRESSURE_UNIT -> CHANNEL_TYPE_PRESSURE;
|
||||
case CHANNEL_TYPE_PERCENT_UNIT -> CHANNEL_TYPE_PERCENT;
|
||||
case CHANNEL_TYPE_TEMPERATURE_UNIT -> CHANNEL_TYPE_TEMPERATURE;
|
||||
case CHANNEL_TYPE_FREQUENCY_UNIT -> CHANNEL_TYPE_FREQUENCY;
|
||||
case CHANNEL_TYPE_FLOW_UNIT -> CHANNEL_TYPE_FLOW;
|
||||
case CHANNEL_TYPE_ELECTRIC_CURRENT_UNIT -> CHANNEL_TYPE_ELECTRIC_CURRENT;
|
||||
case CHANNEL_TYPE_TIME_UNIT -> CHANNEL_TYPE_TIME;
|
||||
default -> isDouble ? CHANNEL_TYPE_DOUBLE : CHANNEL_TYPE_INTEGER;
|
||||
};
|
||||
return new ChannelTypeUID(BINDING_ID, typeName);
|
||||
}
|
||||
|
||||
private ChannelTypeUID determineEnumChannelTypeUID(String channelId, JsonArray enumValues, boolean writable) {
|
||||
var channelTypeUID = determineStaticEnumType(enumValues, writable);
|
||||
if (channelTypeUID == null) {
|
||||
channelTypeUID = getOrBuildDynamicEnumType(channelId, enumValues, writable);
|
||||
}
|
||||
return channelTypeUID;
|
||||
}
|
||||
|
||||
private ChannelTypeUID getOrBuildDynamicEnumType(String channelId, JsonArray enumValues, boolean writable) {
|
||||
final var prefix = writable ? CHANNEL_TYPE_PREFIX_RW + CHANNEL_TYPE_ENUM_PRFIX : CHANNEL_TYPE_ENUM_PRFIX;
|
||||
final var channelTypeUID = new ChannelTypeUID(BINDING_ID, prefix + channelId);
|
||||
var type = channelTypeRegistry.getChannelType(channelTypeUID);
|
||||
|
||||
if (type == null) {
|
||||
var stateBuilder = StateDescriptionFragmentBuilder.create();
|
||||
stateBuilder.withReadOnly(!writable).withOptions(extractEnumValues(enumValues));
|
||||
|
||||
var typeBuilder = ChannelTypeBuilder.state(channelTypeUID, channelId, CoreItemFactory.NUMBER)
|
||||
.withStateDescriptionFragment(stateBuilder.build());
|
||||
|
||||
type = typeBuilder.build();
|
||||
channelTypeProvider.putChannelType(type);
|
||||
}
|
||||
|
||||
return channelTypeUID;
|
||||
}
|
||||
|
||||
private ChannelTypeUID getOrBuildNumberChannelType(String channelId, String unit, @Nullable BigDecimal min,
|
||||
@Nullable BigDecimal max, @Nullable BigDecimal step) {
|
||||
final var channelTypeUID = new ChannelTypeUID(BINDING_ID,
|
||||
CHANNEL_TYPE_PREFIX_RW + CHANNEL_TYPE_NUMERIC_PRFIX + channelId);
|
||||
var type = channelTypeRegistry.getChannelType(channelTypeUID);
|
||||
|
||||
if (type == null) {
|
||||
var stateBuilder = StateDescriptionFragmentBuilder.create().withReadOnly(false);
|
||||
|
||||
if (min != null) {
|
||||
stateBuilder.withMinimum(min);
|
||||
}
|
||||
if (max != null) {
|
||||
stateBuilder.withMaximum(max);
|
||||
}
|
||||
if (step != null) {
|
||||
stateBuilder.withStep(step);
|
||||
}
|
||||
|
||||
var itemType = determineAcceptedType(channelTypeUID, unit);
|
||||
var typeBuilder = ChannelTypeBuilder.state(channelTypeUID, channelId, itemType)
|
||||
.withStateDescriptionFragment(stateBuilder.build());
|
||||
if (!itemType.equals(CoreItemFactory.NUMBER)) {
|
||||
typeBuilder.withUnitHint(unit);
|
||||
}
|
||||
|
||||
channelTypeProvider.putChannelType(typeBuilder.build());
|
||||
}
|
||||
|
||||
return channelTypeUID;
|
||||
}
|
||||
|
||||
List<StateOption> extractEnumValues(JsonArray enumValues) {
|
||||
List<StateOption> list = new ArrayList<>();
|
||||
for (var element : enumValues) {
|
||||
var enumText = Utils.getAsString(element.getAsJsonObject(), JSON_ENUM_KEY_TEXT, EMPTY);
|
||||
var enumOrdinal = Utils.getAsString(element.getAsJsonObject(), JSON_KEY_CHANNEL_VALUE, GENERIC_NO_VAL);
|
||||
list.add(new StateOption(enumOrdinal, StringUtils.capitalizeByWhitespace(enumText.toLowerCase())));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* internal method to dertermine the enum type.
|
||||
*
|
||||
* @param enumValues enum data from myuplink API
|
||||
* @param writable flag to determine writable capability
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private ChannelTypeUID determineStaticEnumType(JsonArray enumValues, boolean writable) {
|
||||
boolean containsOffAt0 = false;
|
||||
boolean containsOnAt1 = false;
|
||||
|
||||
for (var element : enumValues) {
|
||||
var enumText = Utils.getAsString(element.getAsJsonObject(), JSON_ENUM_KEY_TEXT, "").toLowerCase();
|
||||
var enumOrdinal = Utils.getAsString(element.getAsJsonObject(), JSON_KEY_CHANNEL_VALUE, GENERIC_NO_VAL);
|
||||
|
||||
switch (enumText) {
|
||||
case JSON_ENUM_VAL_OFF -> containsOffAt0 = enumOrdinal.equals(JSON_ENUM_ORD_0);
|
||||
case JSON_ENUM_VAL_ON -> containsOnAt1 = enumOrdinal.equals(JSON_ENUM_ORD_1);
|
||||
}
|
||||
}
|
||||
|
||||
if (enumValues.size() == 2 && containsOffAt0 && containsOnAt1) {
|
||||
if (writable) {
|
||||
return new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_RW_SWITCH);
|
||||
} else {
|
||||
return new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_ON_OFF);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.provider;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
import org.openhab.core.thing.binding.AbstractStorageBasedTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelTypeProvider;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Provides generated channel-types to the framework
|
||||
*
|
||||
* @author Alexander Friese - Initial contribution
|
||||
*/
|
||||
@Component(service = { ChannelTypeProvider.class, MyUplinkChannelTypeProvider.class })
|
||||
@NonNullByDefault
|
||||
public class MyUplinkChannelTypeProvider extends AbstractStorageBasedTypeProvider {
|
||||
|
||||
@Activate
|
||||
public MyUplinkChannelTypeProvider(@Reference StorageService storageService) {
|
||||
super(storageService);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="myuplink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
<type>binding</type>
|
||||
<name>myUplink Binding</name>
|
||||
<description>This is the binding for NIBE myUplink.</description>
|
||||
<connection>cloud</connection>
|
||||
</addon:addon>
|
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:myuplink:account">
|
||||
<parameter-group name="authentication">
|
||||
<label>Authentication</label>
|
||||
<description>Authentication settings.</description>
|
||||
</parameter-group>
|
||||
<parameter-group name="connection">
|
||||
<label>Connection</label>
|
||||
<description>Connection settings.</description>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="clientId" type="text" required="true" groupName="authentication">
|
||||
<label>Username</label>
|
||||
<description>The Client Id to login at myUplink.</description>
|
||||
</parameter>
|
||||
<parameter name="clientSecret" type="text" required="true" groupName="authentication">
|
||||
<label>Password</label>
|
||||
<context>password</context>
|
||||
<description>The Client Secret to login at myUplink.</description>
|
||||
</parameter>
|
||||
<parameter name="dataPollingInterval" type="integer" required="false" min="30" max="3600" unit="s"
|
||||
groupName="connection">
|
||||
<label>Polling Interval</label>
|
||||
<description>Interval in which data is polled from myUplink (in seconds).</description>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:myuplink:generic-device">
|
||||
<parameter name="deviceId" type="text" required="true">
|
||||
<label>Device Id</label>
|
||||
<description>The Id to identify the device.</description>
|
||||
</parameter>
|
||||
<parameter name="systemId" type="text" required="false">
|
||||
<label>System Id</label>
|
||||
<description>The Id of the system the device belongs to.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
@ -0,0 +1,80 @@
|
||||
# add-on
|
||||
|
||||
addon.myuplink.name = myUplink Binding
|
||||
addon.myuplink.description = This is the binding for NIBE myUplink.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.myuplink.account.label = myUplink Account
|
||||
thing-type.myuplink.account.description = Cloud connection to a myUplink account.
|
||||
thing-type.myuplink.generic-device.label = myUplink Generic Device
|
||||
thing-type.myuplink.generic-device.description = Cloud connection to a myUplink device.
|
||||
thing-type.myuplink.generic-device.channel.command.label = Generic Command
|
||||
thing-type.myuplink.generic-device.channel.command.description = Allows to send commands to any channel. Format 'channel:value'
|
||||
thing-type.myuplink.generic-device.channel.smart-home-mode.label = Smart Home Mode
|
||||
thing-type.myuplink.generic-device.channel.smart-home-mode.description = Controls the smart home mode
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.myuplink.account.clientId.label = Username
|
||||
thing-type.config.myuplink.account.clientId.description = The Client Id to login at myUplink.
|
||||
thing-type.config.myuplink.account.clientSecret.label = Password
|
||||
thing-type.config.myuplink.account.clientSecret.description = The Client Secret to login at myUplink.
|
||||
thing-type.config.myuplink.account.dataPollingInterval.label = Polling Interval
|
||||
thing-type.config.myuplink.account.dataPollingInterval.description = Interval in which data is polled from myUplink (in seconds).
|
||||
thing-type.config.myuplink.account.group.authentication.label = Authentication
|
||||
thing-type.config.myuplink.account.group.authentication.description = Authentication settings.
|
||||
thing-type.config.myuplink.account.group.connection.label = Connection
|
||||
thing-type.config.myuplink.account.group.connection.description = Connection settings.
|
||||
thing-type.config.myuplink.generic-device.deviceId.label = Device Id
|
||||
thing-type.config.myuplink.generic-device.deviceId.description = The Id to identify the device.
|
||||
thing-type.config.myuplink.generic-device.systemId.label = System Id
|
||||
thing-type.config.myuplink.generic-device.systemId.description = The Id of the system the device belongs to.
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.myuplink.rwtype-command.label = Generic Command
|
||||
channel-type.myuplink.rwtype-mode.label = Generic Command
|
||||
channel-type.myuplink.rwtype-mode.state.option.Default = Default
|
||||
channel-type.myuplink.rwtype-mode.state.option.Normal = Normal
|
||||
channel-type.myuplink.rwtype-mode.state.option.Away = Away
|
||||
channel-type.myuplink.rwtype-mode.state.option.Vacation = Vacation
|
||||
channel-type.myuplink.rwtype-mode.state.option.Home = Home
|
||||
channel-type.myuplink.rwtype-mode.command.option.Default = Default
|
||||
channel-type.myuplink.rwtype-mode.command.option.Normal = Normal
|
||||
channel-type.myuplink.rwtype-mode.command.option.Away = Away
|
||||
channel-type.myuplink.rwtype-mode.command.option.Vacation = Vacation
|
||||
channel-type.myuplink.rwtype-mode.command.option.Home = Home
|
||||
channel-type.myuplink.rwtype-switch.label = Generic Switch
|
||||
channel-type.myuplink.type-electric-current.label = Generic Current
|
||||
channel-type.myuplink.type-energy.label = Generic Energy
|
||||
channel-type.myuplink.type-flow.label = Generic Flow
|
||||
channel-type.myuplink.type-frequency.label = Generic Frequency
|
||||
channel-type.myuplink.type-number-double.label = Generic Number (0.1)
|
||||
channel-type.myuplink.type-number-integer.label = Generic Number
|
||||
channel-type.myuplink.type-on-off.label = Generic OnOff
|
||||
channel-type.myuplink.type-on-off.state.option.0 = Off
|
||||
channel-type.myuplink.type-on-off.state.option.1 = On
|
||||
channel-type.myuplink.type-percent.label = Generic Percentage
|
||||
channel-type.myuplink.type-pressure.label = Generic Pressure
|
||||
channel-type.myuplink.type-temperature.label = Generic Temperature
|
||||
channel-type.myuplink.type-time.label = Generic Time
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.myuplink.account.group.customChannels.label = Custom Channels
|
||||
thing-type.config.myuplink.account.group.customChannels.description = Custom Channel configuration
|
||||
thing-type.config.myuplink.account.group.general.label = General
|
||||
thing-type.config.myuplink.account.group.general.description = General settings.
|
||||
|
||||
|
||||
# status translations
|
||||
|
||||
status.token.validated = "Access token validated"
|
||||
status.waiting.for.bridge = "Waiting for bridge to go online"
|
||||
status.waiting.for.login = "Waiting for web api login"
|
||||
status.no.valid.data = "No valid data received. This is most likely a configuration error"
|
||||
status.no.connection = "No connection could be established"
|
||||
status.device.not.found = "Device could not be found. Most likely a configuration error"
|
||||
status.config.error.no.client.id = "No/Empty client id. Please fix the configuration."
|
||||
status.config.error.no.client.secret = "No/Empty client secret. Please fix the configuration."
|
@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="myuplink"
|
||||
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">
|
||||
|
||||
<channel-type id="type-number-integer" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Generic Number</label>
|
||||
<state pattern="%d" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-number-double" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Generic Number (0.1)</label>
|
||||
<state pattern="%.1f" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Generic Temperature</label>
|
||||
<state pattern="%.1f %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-flow">
|
||||
<item-type>Number:VolumetricFlowRate</item-type>
|
||||
<label>Generic Flow</label>
|
||||
<state pattern="%.1f %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-electric-current" advanced="true">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>Generic Current</label>
|
||||
<state pattern="%.1f %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-time">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Generic Time</label>
|
||||
<state pattern="%d %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-frequency">
|
||||
<item-type>Number:Frequency</item-type>
|
||||
<label>Generic Frequency</label>
|
||||
<state pattern="%d %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-energy">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Generic Energy</label>
|
||||
<state pattern="%.1f %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-pressure" advanced="true">
|
||||
<item-type>Number:Pressure</item-type>
|
||||
<label>Generic Pressure</label>
|
||||
<state pattern="%.1f %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-percent">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Generic Percentage</label>
|
||||
<state pattern="%d %%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="type-on-off">
|
||||
<item-type>Number</item-type>
|
||||
<label>Generic OnOff</label>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">Off</option>
|
||||
<option value="1">On</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="myuplink"
|
||||
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">
|
||||
|
||||
<channel-type id="rwtype-switch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Generic Switch</label>
|
||||
<state readOnly="false"></state>
|
||||
</channel-type>
|
||||
<channel-type id="rwtype-command" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Generic Command</label>
|
||||
<state readOnly="false"></state>
|
||||
</channel-type>
|
||||
<channel-type id="rwtype-mode">
|
||||
<item-type>String</item-type>
|
||||
<label>Generic Command</label>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="Default">Default</option>
|
||||
<option value="Normal">Normal</option>
|
||||
<option value="Away">Away</option>
|
||||
<option value="Vacation">Vacation</option>
|
||||
<option value="Home">Home</option>
|
||||
</options>
|
||||
</state>
|
||||
<command>
|
||||
<options>
|
||||
<option value="Default">Default</option>
|
||||
<option value="Normal">Normal</option>
|
||||
<option value="Away">Away</option>
|
||||
<option value="Vacation">Vacation</option>
|
||||
<option value="Home">Home</option>
|
||||
</options>
|
||||
</command>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="myuplink"
|
||||
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="account">
|
||||
<label>myUplink Account</label>
|
||||
<description>Cloud connection to a myUplink account.</description>
|
||||
<config-description-ref uri="thing-type:myuplink:account"/>
|
||||
</bridge-type>
|
||||
<thing-type id="generic-device">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>myUplink Generic Device</label>
|
||||
<description>Cloud connection to a myUplink device.</description>
|
||||
<channels>
|
||||
<channel id="command" typeId="rwtype-command">
|
||||
<label>Generic Command</label>
|
||||
<description>Allows to send commands to any channel. Format 'channel:value'</description>
|
||||
<properties>
|
||||
<property name="validationExpression">[0-9]+:-?[0-9,]+</property>
|
||||
</properties>
|
||||
</channel>
|
||||
<channel id="smart-home-mode" typeId="rwtype-mode">
|
||||
<label>Smart Home Mode</label>
|
||||
<description>Controls the smart home mode</description>
|
||||
<properties>
|
||||
<property name="validationExpression">\w+</property>
|
||||
</properties>
|
||||
</channel>
|
||||
</channels>
|
||||
<config-description-ref uri="thing-type:myuplink:generic-device"/>
|
||||
</thing-type>
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.discovery;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
|
||||
import org.openhab.binding.myuplink.internal.handler.MyUplinkAccountHandler;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* Unit Tests to verify behaviour of DiscoveryService implementation.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class MyUplinkDiscoveryServiceTest {
|
||||
|
||||
private MyUplinkAccountHandler bridgeHandler = mock(MyUplinkAccountHandler.class);
|
||||
|
||||
private CommunicationStatus communicationStatus = mock(CommunicationStatus.class);
|
||||
|
||||
private MyUplinkDiscoveryService discoveryService = spy(MyUplinkDiscoveryService.class);
|
||||
|
||||
private final String emptyResponseString = """
|
||||
{"page":1,"itemsPerPage":100,"numItems":0,"systems":[]}
|
||||
""";
|
||||
|
||||
private JsonObject emptyResponse = new JsonObject();
|
||||
|
||||
private final String testResponseString = """
|
||||
{
|
||||
"page": 0,
|
||||
"itemsPerPage": 0,
|
||||
"numItems": 0,
|
||||
"systems": [
|
||||
{
|
||||
"systemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"name": "string",
|
||||
"securityLevel": "admin",
|
||||
"hasAlarm": true,
|
||||
"country": "string",
|
||||
"devices": [
|
||||
{
|
||||
"id": "Dev-1337",
|
||||
"connectionState": "Disconnected",
|
||||
"currentFwVersion": "string",
|
||||
"product": {
|
||||
"serialNumber": "1337",
|
||||
"name": "My Device 1337"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Dev-4712",
|
||||
"connectionState": "Disconnected",
|
||||
"currentFwVersion": "string",
|
||||
"product": {
|
||||
"serialNumber": "4712",
|
||||
"name": "My Device 4712"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private static JsonObject testResponse = new JsonObject();
|
||||
|
||||
@BeforeEach
|
||||
public void prepareTestData() {
|
||||
emptyResponse = JsonParser.parseString(emptyResponseString).getAsJsonObject();
|
||||
testResponse = JsonParser.parseString(testResponseString).getAsJsonObject();
|
||||
|
||||
discoveryService.setThingHandler(bridgeHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyResponse() {
|
||||
discoveryService.processMyUplinkDiscoveryResult(communicationStatus, emptyResponse);
|
||||
|
||||
// testdata contains no systems -> no further processing
|
||||
verify(discoveryService, never()).handleSystemDiscovery(any());
|
||||
verify(discoveryService, never()).handleDeviceDiscovery(any(), any());
|
||||
verify(discoveryService, never()).initDiscoveryResultBuilder(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSampleResponse() {
|
||||
// mocking of bridgehandler needed to get an UID.
|
||||
Bridge mockThing = mock(Bridge.class);
|
||||
when(mockThing.getUID()).thenReturn(new ThingUID(THING_TYPE_ACCOUNT, "testAccount4711"));
|
||||
when(bridgeHandler.getThing()).thenReturn(mockThing);
|
||||
|
||||
discoveryService.processMyUplinkDiscoveryResult(communicationStatus, testResponse);
|
||||
|
||||
// testdata contains one system
|
||||
verify(discoveryService, times(1)).handleSystemDiscovery(any());
|
||||
// testdata contains two devices
|
||||
verify(discoveryService, times(2)).handleDeviceDiscovery(any(), any());
|
||||
// builder should be called once for each device
|
||||
verify(discoveryService, times(2)).initDiscoveryResultBuilder(any(), any(), any());
|
||||
|
||||
// verify that correct values are extracted from data
|
||||
verify(discoveryService).initDiscoveryResultBuilder(DEVICE_GENERIC_DEVICE, "Dev-4712", "My Device 4712");
|
||||
verify(discoveryService).initDiscoveryResultBuilder(DEVICE_GENERIC_DEVICE, "Dev-1337", "My Device 1337");
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myuplink.internal.provider;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.myuplink.internal.MyUplinkBindingConstants;
|
||||
import org.openhab.binding.myuplink.internal.Utils;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.test.storage.VolatileStorageService;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Unit Tests to verify behaviour of ChannelFactory implementation.
|
||||
*
|
||||
* @author Alexander Friese - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelFactoryTest {
|
||||
|
||||
private final MyUplinkChannelTypeProvider channelTypeProvider = new MyUplinkChannelTypeProvider(
|
||||
new VolatileStorageService());
|
||||
private final ChannelFactory channelFactory = new ChannelFactory(channelTypeProvider, new ChannelTypeRegistry());
|
||||
|
||||
private static final ThingUID TEST_THING_UID = new ThingUID(MyUplinkBindingConstants.BINDING_ID, "genericThing",
|
||||
"myUnit");
|
||||
|
||||
private final String testChannelDataTemperature = """
|
||||
{"category":"NIBEF VVM 320 E","parameterId":"40121","parameterName":"Add. heat (BT63)","parameterUnit":"°C","writable":false,"timestamp":"2024-05-10T05:35:50+00:00","value":39.0,"strVal":"39°C","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[],"scaleValue":"0.1","zoneId":null}
|
||||
""";
|
||||
|
||||
private final String testChannelEnumWritableSwitch = """
|
||||
{"category":"NIBEF VVM 320 E","parameterId":"50004","parameterName":"Temporary lux","parameterUnit":"","writable":true,"timestamp":"2024-05-05T13:41:09+00:00","value":0.0,"strVal":"off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"0","text":"off","icon":""},{"value":"1","text":"on","icon":""}],"scaleValue":"1","zoneId":null}
|
||||
""";
|
||||
|
||||
private final String testChannelEnumSwitch = """
|
||||
{"category":"NIBEF VVM 320 E","parameterId":"49992","parameterName":"Pump: Heating medium (GP6)","parameterUnit":"","writable":false,"timestamp":"2024-05-05T13:41:09+00:00","value":0.0,"strVal":"Off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"0","text":"Off","icon":""},{"value":"1","text":"On","icon":""}],"scaleValue":"1","zoneId":null}
|
||||
""";
|
||||
|
||||
private final String testChannelEnumPriority = """
|
||||
{"category":"NIBEF VVM 320 E","parameterId":"49994","parameterName":"Priority","parameterUnit":"","writable":false,"timestamp":"2024-05-10T03:31:23+00:00","value":10.0,"strVal":"Off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"10","text":"Off","icon":""},{"value":"20","text":"Hot water","icon":""},{"value":"30","text":"Heating","icon":""},{"value":"40","text":"Pool","icon":""},{"value":"41","text":"Pool 2","icon":""},{"value":"50","text":"TransÂfer","icon":""},{"value":"60","text":"Cooling","icon":""}],"scaleValue":"1","zoneId":null}
|
||||
""";
|
||||
|
||||
private final String testChannelEnumCompressorStatus = """
|
||||
{"category":"Slave 1 (EB101)","parameterId":"44064","parameterName":"Status compressor (EB101)","parameterUnit":"","writable":false,"timestamp":"2024-05-10T03:31:28+00:00","value":20.0,"strVal":"off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"20","text":"off","icon":""},{"value":"40","text":"starts","icon":""},{"value":"60","text":"runs","icon":""},{"value":"100","text":"stops","icon":""}],"scaleValue":"1","zoneId":null}
|
||||
""";
|
||||
|
||||
private final String testChannelEnumAddHeatStatus = """
|
||||
{"category":"NIBEF VVM 320 E","parameterId":"49993","parameterName":"Int elec add heat","parameterUnit":"","writable":false,"timestamp":"2024-05-05T13:41:27+00:00","value":4.0,"strVal":"Blocked","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"0","text":"Alarm","icon":""},{"value":"1","text":"Alarm","icon":""},{"value":"2","text":"Active","icon":""},{"value":"3","text":"Off","icon":""},{"value":"4","text":"Blocked","icon":""},{"value":"5","text":"Off","icon":""},{"value":"6","text":"Active","icon":""}],"scaleValue":"1","zoneId":null}
|
||||
""";
|
||||
|
||||
private final String testChannelEnumHeatPumpStatusWithLowerCaseTexts = """
|
||||
{ "category": "Heat pump 1", "parameterId": "62017", "parameterName": "Status", "parameterUnit": "", "writable": false, "timestamp": "2024-05-21T16:22:21+00:00", "value": 1, "strVal": "Off, ready to start", "smartHomeCategories": [], "minValue": null, "maxValue": null, "stepValue": 1, "enumValues": [ { "value": "0", "text": "Off, start delay", "icon": "" }, { "value": "1", "text": "OFF, ready to start", "icon": "" }, { "value": "2", "text": "Wait until flow", "icon": "" }, { "value": "3", "text": "On", "icon": "" }, { "value": "4", "text": "Defrost", "icon": "" }, { "value": "5", "text": "Cooling", "icon": "" }, { "value": "6", "text": "Blocked", "icon": "" }, { "value": "7", "text": "Off, alarm", "icon": "" }, { "value": "8", "text": "Function test", "icon": "" }, { "value": "30", "text": "not defined", "icon": "" }, { "value": "31", "text": "Comp. disabled", "icon": "" }, { "value": "32", "text": "Comm. error", "icon": "" }, { "value": "33", "text": "Hot Water", "icon": "" } ], "scaleValue": "1", "zoneId": null }
|
||||
""";
|
||||
|
||||
@Test
|
||||
public void testFromJsonDataTemperature() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelDataTemperature, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var result = channelFactory.createChannel(TEST_THING_UID, json);
|
||||
assertThat(result.getAcceptedItemType(), is("Number:Temperature"));
|
||||
assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-temperature"));
|
||||
assertThat(result.getUID().getThingUID(), is(TEST_THING_UID));
|
||||
assertThat(result.getUID().getId(), is("40121"));
|
||||
assertThat(result.getDescription(), is("Add. heat (BT63)"));
|
||||
assertThat(result.getLabel(), is("Add. heat (BT63)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromJsonDataEnumWritableSwitch() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelEnumWritableSwitch, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var result = channelFactory.createChannel(TEST_THING_UID, json);
|
||||
assertThat(result.getAcceptedItemType(), is(CoreItemFactory.SWITCH));
|
||||
assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("rwtype-switch"));
|
||||
assertThat(result.getUID().getThingUID(), is(TEST_THING_UID));
|
||||
assertThat(result.getUID().getId(), is("50004"));
|
||||
assertThat(result.getDescription(), is("Temporary lux"));
|
||||
assertThat(result.getLabel(), is("Temporary lux"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromJsonDataEnumSwitch() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelEnumSwitch, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var result = channelFactory.createChannel(TEST_THING_UID, json);
|
||||
assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
|
||||
assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-on-off"));
|
||||
assertThat(result.getUID().getThingUID(), is(TEST_THING_UID));
|
||||
assertThat(result.getUID().getId(), is("49992"));
|
||||
assertThat(result.getDescription(), is("Pump: Heating medium (GP6)"));
|
||||
assertThat(result.getLabel(), is("Pump: Heating medium (GP6)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromJsonDataEnumPriority() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelEnumPriority, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var result = channelFactory.createChannel(TEST_THING_UID, json);
|
||||
assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
|
||||
assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-49994"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromJsonDataEnumCompressorStatus() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelEnumCompressorStatus, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var result = channelFactory.createChannel(TEST_THING_UID, json);
|
||||
assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
|
||||
assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-44064"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromJsonDataEnumAddHeatStatus() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelEnumAddHeatStatus, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var result = channelFactory.createChannel(TEST_THING_UID, json);
|
||||
assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
|
||||
assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-49993"));
|
||||
|
||||
var type = channelTypeProvider.getChannelType(new ChannelTypeUID(BINDING_ID, "type-enum-49993"), null);
|
||||
assertNotNull(type);
|
||||
assertThat(Objects.requireNonNull(type.getState()).getOptions().size(), is(7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromJsonDataEnumHeatPumpStatus() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelEnumHeatPumpStatusWithLowerCaseTexts, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var result = channelFactory.createChannel(TEST_THING_UID, json);
|
||||
assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
|
||||
assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-62017"));
|
||||
|
||||
var type = channelTypeProvider.getChannelType(new ChannelTypeUID(BINDING_ID, "type-enum-62017"), null);
|
||||
assertNotNull(type);
|
||||
assertThat(Objects.requireNonNull(type.getState()).getOptions().size(), is(13));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeatPumpStatusEnumValues() {
|
||||
var gson = new Gson();
|
||||
var json = gson.fromJson(testChannelEnumHeatPumpStatusWithLowerCaseTexts, JsonObject.class);
|
||||
json = json == null ? new JsonObject() : json;
|
||||
|
||||
var enumValues = Utils.getAsJsonArray(json, JSON_KEY_CHANNEL_ENUM_VALUES);
|
||||
var list = channelFactory.extractEnumValues(enumValues);
|
||||
|
||||
assertThat(list.size(), is(13));
|
||||
|
||||
list.forEach(enumMapping -> {
|
||||
switch (enumMapping.getValue()) {
|
||||
case "0" -> assertThat(enumMapping.getLabel(), is("Off, Start Delay"));
|
||||
case "1" -> assertThat(enumMapping.getLabel(), is("Off, Ready To Start"));
|
||||
case "2" -> assertThat(enumMapping.getLabel(), is("Wait Until Flow"));
|
||||
case "3" -> assertThat(enumMapping.getLabel(), is("On"));
|
||||
case "4" -> assertThat(enumMapping.getLabel(), is("Defrost"));
|
||||
case "5" -> assertThat(enumMapping.getLabel(), is("Cooling"));
|
||||
case "6" -> assertThat(enumMapping.getLabel(), is("Blocked"));
|
||||
case "7" -> assertThat(enumMapping.getLabel(), is("Off, Alarm"));
|
||||
case "8" -> assertThat(enumMapping.getLabel(), is("Function Test"));
|
||||
case "30" -> assertThat(enumMapping.getLabel(), is("Not Defined"));
|
||||
case "31" -> assertThat(enumMapping.getLabel(), is("Comp. Disabled"));
|
||||
case "32" -> assertThat(enumMapping.getLabel(), is("Comm. Error"));
|
||||
case "33" -> assertThat(enumMapping.getLabel(), is("Hot Water"));
|
||||
default -> assertNotNull(null, "unknown enum value");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -285,6 +285,7 @@
|
||||
<module>org.openhab.binding.mycroft</module>
|
||||
<module>org.openhab.binding.mynice</module>
|
||||
<module>org.openhab.binding.mystrom</module>
|
||||
<module>org.openhab.binding.myuplink</module>
|
||||
<module>org.openhab.binding.nanoleaf</module>
|
||||
<module>org.openhab.binding.neato</module>
|
||||
<module>org.openhab.binding.neeo</module>
|
||||
|
Loading…
Reference in New Issue
Block a user