diff --git a/bundles/org.openhab.binding.elroconnects/README.md b/bundles/org.openhab.binding.elroconnects/README.md index c2937c63d8a..7f2a0093dfc 100644 --- a/bundles/org.openhab.binding.elroconnects/README.md +++ b/bundles/org.openhab.binding.elroconnects/README.md @@ -4,10 +4,12 @@ The ELRO Connects binding provides integration with the [ELRO Connects](https:// The system uses a Wi-Fi Hub (K1 Connector) to enable communication with various smart home devices. The devices communicate with the hub using 868MHz RF. -The binding only communicates with the ELRO Connects system and K1 Connector using UDP in the local network. +The binding communicates with the ELRO Connects system and K1 Connector using UDP in the local network. +Optionally, the Elro Connects account can be used to retrieve the available K1 Connectors with their properties from the ELRO Connects cloud. The binding exposes the devices' status and controls to openHAB. -The K1 connector itself allows setting up scenes through a mobile application. +Console commands support adding and configuring devices on the hub. +The K1 connector allows setting up scenes through a mobile application. The binding supports selecting a specific scene. Many of the sensor devices are battery powered. @@ -16,6 +18,7 @@ Many of the sensor devices are battery powered. The ELRO Connects supported device types are: +* Elro Connects account: `account` * K1 connector hub: `connector` * Smoke detector: `smokealarm` * Carbon monoxide detector: `coalarm` @@ -26,16 +29,27 @@ The ELRO Connects supported device types are: * Temperature and humidity monitor: `temperaturesensor` * Plug-in switch: `powersocket` -The `connector` is the bridge thing. -All other things are connected to the bridge. +`account` is a bridge thing type that will allow automatic discovery and configuration of the available K1 connectors on the specified ELRO Connects account. +This bridge is optional. +It is used to discover the required K1 connector hub(s), using a call to the ELRO Connects cloud. +Without the `account` bridge, the `connector` bridge needs to be defined manually. +If no `account` bridge is defined, all communication between openHAB and the ELRO Connects system will be local, not using the ELRO Connects cloud. + +The `connector` is the bridge thing representing a K1 connector. +All other things are connected to the `connector` bridge. Testing was only done with smoke and water detectors connected to a K1 connector. The firmware version of the K1 connector was 2.0.3.30 at the time of testing. -Older versions of the firmware are known to have differences in the communication protocol. ## Discovery -The K1 connector `connector` cannot be auto-discovered. +The ELRO Connects `account` cannot be auto-discovered. +The `account` bridge is optional, but helpful to discover the K1 connectors on an ELRO Connects account and configure them. +All online K1 connectors configured on the account will be discovered. +Notice that K1 connectors in another network than the LAN will also get discovered, but will not go online when accepted from the inbox without adjusting the `connector` configuration (set the IP address). + +The K1 connector `connector` will be auto-discovered when an ELRO Connects `account` bridge has been created and initialized. +It can also be configured manually without first setting up an `account` bridge and linking it to that `account` bridge. Once the bridge thing representing the K1 connector is correctly set up and online, discovery will allow discovering all devices connected to the K1 connector (as set up in the Elro Connects app). If devices are outside reliable RF range, devices known to the K1 hub will be discovered but may stay offline when added as a thing. @@ -44,89 +58,115 @@ It will not be possible to receive alarms and control them from openHAB in this ## Thing Configuration +### ELRO Connects account + +| Parameter | Advanced | Description | +|-----------------------------|:--------:|--------------------------------------------------------| +| `username` | | Username for the ELRO Connects cloud account, required | +| `password` | | Password for the ELRO Connects cloud account, required | +| `enableBackgroundDiscovery` | Y | Enable background discovery of hubs, polling the ELRO Connects cloud account every 5 min. | + ### K1 connector hub -| Parameter | Advanced | Description | -|-------------------|:--------:|------------------------| -| `connectorId` | | Required parameter, should be set to ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC address of the connector. This parameter can also be found in the ELRO Connects mobile application. | -| `ipAdress` | Y | IP address of the ELRO Connects K1 Connector, not required if connector and openHAB server in same subnet. | -| `refreshInterval` | Y | This parameter controls the connection refresh heartbeat interval. The default is 60s. | +| Parameter | Advanced | Description | +|-----------------------------|:--------:|--------------------------------------------------------| +| `connectorId` | | Required parameter, should be set to ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC address of the connector. It will be discovered when an `account` bridge has been initialized. This parameter can also be found in the ELRO Connects mobile application | +| `ipAdress` | Y | IP address of the ELRO Connects K1 Connector, not required if connector and openHAB server in same subnet | +| `refreshInterval` | Y | This parameter controls the connection refresh heartbeat interval. The default is 60s | +| `legacyFirmware` | Y | Flag for legacy firmware, should be set to true if ELRO Connects K1 Connector firmware has version lower or equal to 2.0.14. If the connector is discovered from the account, this parameter will be set automatically. The default is false | ### Devices connected to K1 connected hub -| Parameter | Description | -|--------------------|----------------------| -| `deviceId` | Required parameter, set by discovery and cannot easily be found manually. It should be a number. | +| Parameter | Advanced | Description | +|-----------------------------|:--------:|--------------------------------------------------------| +| `deviceId` | | Required parameter, set by discovery. For manual configuration, use the ´elroconnects devices´ console command to get a list of available devices. It should be a number | ## Channels +### ELRO Connects account + +The `account` bridge thing does not have any channels. + ### K1 connector hub The `connector` bridge thing has only one channel: -| Channel ID | Item Type | Access Mode | Description | -|--------------------|----------------------|:-----------:|----------------------------------------------------| -| `scene` | String | RW | current scene | +| Channel ID | Item Type | Access Mode | Description | +|--------------------|----------------|:-----------:|----------------------------------------------------| +| `scene` | String | RW | current scene | The `scene` channel has a dynamic state options list with all possible scene choices available in the hub. -## Smoke, carbon monoxide, heat and water alarms +The `connector` also has an `alarm` trigger channel that will get triggered when the alarm is triggered for any device connected to the hub. +This will also trigger if an alarm on a device goes off and the thing corresponding to the device is not configured in openHAB. +The payload for the trigger channel is the `deviceId` for the device triggering the alarm. + +### Smoke, carbon monoxide, heat and water alarms All these things have the same channels: -| Channel ID | Item Type | Access Mode | Description | -|--------------------|----------------------|:-----------:|----------------------------------------------------| -| `muteAlarm` | Switch | RW | mute alarm | -| `testAlarm` | Switch | RW | test alarm | -| `battery` | Number | R | battery level in % | -| `lowBattery` | Switch | R | on for low battery (below 15%) | +| Channel ID | Item Type | Access Mode | Description | +|--------------------|----------------------|:-----------:|---------------------------------------------| +| `muteAlarm` | Switch | RW | mute alarm | +| `testAlarm` | Switch | RW | test alarm | +| `signal` | Number | R | signal strength between 0 and 4, higher is stronger | +| `battery` | Number | R | battery level in % | +| `lowBattery` | Switch | R | on for low battery (below 15%) | Each also has a trigger channel, resp. `smokeAlarm`, `coAlarm`, `heatAlarm` and `waterAlarm`. The payload for these trigger channels is empty. -## Door/window contact +### Door/window contact The `entrysensor` thing has the following channels: -| Channel ID | Item Type | Access Mode | Description | -|--------------------|----------------------|:-----------:|----------------------------------------------------| -| `entry` | Contact | R | open/closed door/window | -| `battery` | Number | R | battery level in % | -| `lowBattery` | Switch | R | on for low battery (below 15%) | +| Channel ID | Item Type | Access Mode | Description | +|--------------------|----------------------|:-----------:|---------------------------------------------| +| `entry` | Contact | R | open/closed door/window | +| `signal` | Number | R | signal strength between 0 and 4, higher is stronger | +| `battery` | Number | R | battery level in % | +| `lowBattery` | Switch | R | on for low battery (below 15%) | The `entrysensor` thing also has a trigger channel, `entryAlarm`. -## Motion sensor +### Motion sensor The `motionsensor` thing has the following channels: -| Channel ID | Item Type | Access Mode | Description | -|--------------------|----------------------|:-----------:|----------------------------------------------------| -| `motion` | Switch | R | on when motion detected | -| `battery` | Number | R | battery level in % | -| `lowBattery` | Switch | R | on for low battery (below 15%) | +| Channel ID | Item Type | Access Mode | Description | +|--------------------|----------------------|:-----------:|---------------------------------------------| +| `motion` | Switch | R | on when motion detected | +| `signal` | Number | R | signal strength between 0 and 4, higher is stronger | +| `battery` | Number | R | battery level in % | +| `lowBattery` | Switch | R | on for low battery (below 15%) | The `motionsensor` thing also has a trigger channel, `motionAlarm`. -## Temperature and humidity monitor +### Temperature and humidity monitor The `temperaturesensor` thing has the following channels: -| Channel ID | Item Type | Access Mode | Description | -|--------------------|----------------------|:-----------:|----------------------------------------------------| -| `temperature` | Number:Temperature | R | temperature | -| `humidity` | Number:Dimensionless | R | device status | -| `battery` | Number | R | battery level in % | -| `lowBattery` | Switch | R | on for low battery (below 15%) | +| Channel ID | Item Type | Access Mode | Description | +|--------------------|----------------------|:-----------:|---------------------------------------------| +| `temperature` | Number:Temperature | R | temperature | +| `humidity` | Number:Dimensionless | R | device status | +| `signal` | Number | R | signal strength between 0 and 4, higher is stronger | +| `battery` | Number | R | battery level in % | +| `lowBattery` | Switch | R | on for low battery (below 15%) | -## Plug-in switch +### Plug-in switch The `powersocket` thing has only one channel: -| Channel ID | Item Type | Access Mode | Description | -|--------------------|----------------------|:-----------:|----------------------------------------------------| -| `powerState` | Switch | RW | power on/off | +| Channel ID | Item Type | Access Mode | Description | +|--------------------|----------------------|:-----------:|---------------------------------------------| +| `powerState` | Switch | RW | power on/off | +## Console Commands + +A number of console commands allow management of the Elro Connects K1 hub and devices. +This makes it possible to add new devices to the hub, remove, rename or replace devices, without a need to use the Elro Connects mobile application. +The full syntax and help text is available in the console using the `elroconnects` command. ## Full Example @@ -172,5 +212,3 @@ then ... end ``` - - diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsBindingConstants.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsBindingConstants.java index a2c0d57cbb7..073c6b2cc91 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsBindingConstants.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsBindingConstants.java @@ -15,6 +15,7 @@ package org.openhab.binding.elroconnects.internal; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -28,9 +29,12 @@ import org.openhab.core.thing.ThingTypeUID; @NonNullByDefault public class ElroConnectsBindingConstants { - private static final String BINDING_ID = "elroconnects"; + static final String BINDING_ID = "elroconnects"; // List of all Thing Type UIDs + public static final String TYPE_ACCOUNT = "account"; + public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, TYPE_ACCOUNT); + public static final String TYPE_CONNECTOR = "connector"; public static final ThingTypeUID THING_TYPE_CONNECTOR = new ThingTypeUID(BINDING_ID, TYPE_CONNECTOR); @@ -51,14 +55,22 @@ public class ElroConnectsBindingConstants { public static final ThingTypeUID THING_TYPE_THSENSOR = new ThingTypeUID(BINDING_ID, TYPE_THSENSOR); public static final ThingTypeUID THING_TYPE_POWERSOCKET = new ThingTypeUID(BINDING_ID, TYPE_POWERSOCKET); - public static final Set SUPPORTED_BRIDGE_TYPES_UIDS = Set.of(THING_TYPE_CONNECTOR); - public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_CONNECTOR, - THING_TYPE_SMOKEALARM, THING_TYPE_COALARM, THING_TYPE_HEATALARM, THING_TYPE_WATERALARM, - THING_TYPE_ENTRYSENSOR, THING_TYPE_MOTIONSENSOR, THING_TYPE_THSENSOR, THING_TYPE_POWERSOCKET); + public static final Set SUPPORTED_ACCOUNT_TYPE_UIDS = Set.of(THING_TYPE_ACCOUNT); + public static final Set SUPPORTED_CONNECTOR_TYPES_UIDS = Set.of(THING_TYPE_CONNECTOR); + public static final Set SUPPORTED_DEVICE_TYPES_UIDS = Set.of(THING_TYPE_SMOKEALARM, + THING_TYPE_COALARM, THING_TYPE_HEATALARM, THING_TYPE_WATERALARM, THING_TYPE_ENTRYSENSOR, + THING_TYPE_MOTIONSENSOR, THING_TYPE_THSENSOR, THING_TYPE_POWERSOCKET); + public static final Set SUPPORTED_BRIDGE_TYPES_UIDS = Stream + .concat(SUPPORTED_ACCOUNT_TYPE_UIDS.stream(), SUPPORTED_CONNECTOR_TYPES_UIDS.stream()) + .collect(Collectors.toSet()); + public static final Set SUPPORTED_THING_TYPES_UIDS = Stream + .concat(SUPPORTED_BRIDGE_TYPES_UIDS.stream(), SUPPORTED_DEVICE_TYPES_UIDS.stream()) + .collect(Collectors.toSet()); // List of all Channel ids public static final String SCENE = "scene"; + public static final String SIGNAL_STRENGTH = "signal"; public static final String BATTERY_LEVEL = "battery"; public static final String LOW_BATTERY = "lowBattery"; public static final String MUTE_ALARM = "muteAlarm"; @@ -72,6 +84,7 @@ public class ElroConnectsBindingConstants { public static final String TEMPERATURE = "temperature"; public static final String HUMIDITY = "humidity"; + public static final String ALARM = "alarm"; public static final String SMOKE_ALARM = "smokeAlarm"; public static final String CO_ALARM = "coAlarm"; public static final String HEAT_ALARM = "heatAlarm"; @@ -80,8 +93,12 @@ public class ElroConnectsBindingConstants { public static final String MOTION_ALARM = "motionAlarm"; // Config properties + public static final String CONFIG_USERNAME = "username"; + public static final String CONFIG_PASSWORD = "password"; public static final String CONFIG_CONNECTOR_ID = "connectorId"; + public static final String CONFIG_IP_ADDRESS = "ipAddress"; public static final String CONFIG_REFRESH_INTERVAL_S = "refreshInterval"; + public static final String CONFIG_LEGACY_FIRMWARE = "legacyFirmware"; public static final String CONFIG_DEVICE_ID = "deviceId"; public static final String CONFIG_DEVICE_TYPE = "deviceType"; @@ -90,20 +107,34 @@ public class ElroConnectsBindingConstants { public static final int ELRO_GET_DEVICE_NAME = 14; public static final int ELRO_GET_DEVICE_STATUSES = 15; public static final int ELRO_REC_DEVICE_NAME = 17; - public static final int ELRO_REC_DEVICE_STATUS = 19; + public static final int ELRO_REC_DEVICE_STATUS = 119; public static final int ELRO_SYNC_DEVICES = 29; + public static final int ELRO_DEVICE_JOIN = 2; + public static final int ELRO_DEVICE_CANCEL_JOIN = 7; + public static final int ELRO_DEVICE_REPLACE = 103; + public static final int ELRO_DEVICE_REMOVE = 104; + public static final int ELRO_DEVICE_RENAME = 105; + public static final int ELRO_SELECT_SCENE = 106; public static final int ELRO_GET_SCENE = 18; - public static final int ELRO_REC_SCENE = 28; - public static final int ELRO_REC_SCENE_NAME = 26; - public static final int ELRO_REC_SCENE_TYPE = 27; + public static final int ELRO_REC_SCENE = 128; + public static final int ELRO_REC_SCENE_NAME = 126; + public static final int ELRO_REC_SCENE_TYPE = 127; public static final int ELRO_SYNC_SCENES = 131; public static final int ELRO_REC_ALARM = 25; public static final int ELRO_IGNORE_YES_NO = 11; + // Older firmware uses different cmd message codes + public static final Map ELRO_LEGACY_MESSAGES = Map.ofEntries(Map.entry(ELRO_DEVICE_CONTROL, 1), + Map.entry(ELRO_DEVICE_REPLACE, 3), Map.entry(ELRO_DEVICE_REMOVE, 4), Map.entry(ELRO_DEVICE_RENAME, 5), + Map.entry(ELRO_SELECT_SCENE, 6), Map.entry(ELRO_SYNC_SCENES, 31), Map.entry(ELRO_REC_DEVICE_STATUS, 19), + Map.entry(ELRO_REC_SCENE, 28), Map.entry(ELRO_REC_SCENE_NAME, 26), Map.entry(ELRO_REC_SCENE_TYPE, 27)); + public static final Map ELRO_NEW_MESSAGES = ELRO_LEGACY_MESSAGES.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Map.Entry::getValue, Map.Entry::getKey)); + // ELRO device types public static enum ElroDeviceType { ENTRY_SENSOR, @@ -129,6 +160,15 @@ public class ElroConnectsBindingConstants { Map.entry(ElroDeviceType.TH_SENSOR, THING_TYPE_THSENSOR), Map.entry(ElroDeviceType.POWERSOCKET, THING_TYPE_POWERSOCKET)); + public static final Map TYPE_NAMES = Map.ofEntries( + Map.entry(ElroDeviceType.ENTRY_SENSOR, TYPE_ENTRYSENSOR), Map.entry(ElroDeviceType.CO_ALARM, TYPE_COALARM), + Map.entry(ElroDeviceType.CXSM_ALARM, TYPE_SMOKEALARM), + Map.entry(ElroDeviceType.MOTION_SENSOR, TYPE_MOTIONSENSOR), + Map.entry(ElroDeviceType.SM_ALARM, TYPE_SMOKEALARM), + Map.entry(ElroDeviceType.THERMAL_ALARM, TYPE_HEATALARM), + Map.entry(ElroDeviceType.WT_ALARM, TYPE_WATERALARM), Map.entry(ElroDeviceType.TH_SENSOR, TYPE_THSENSOR), + Map.entry(ElroDeviceType.POWERSOCKET, TYPE_POWERSOCKET)); + public static final Set T_ENTRY_SENSOR = Set.of("0101", "1101", "2101"); public static final Set T_POWERSOCKET = Set.of("0200", "1200", "2200"); public static final Set T_MOTION_SENSOR = Set.of("0100", "1100", "2100"); diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsHandlerFactory.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsHandlerFactory.java index adf97df35ac..553e02d3d26 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsHandlerFactory.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsHandlerFactory.java @@ -16,6 +16,9 @@ import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConst import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler; +import org.openhab.binding.elroconnects.internal.handler.ElroConnectsAccountHandler; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsBridgeHandler; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsCOAlarmHandler; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsDeviceHandler; @@ -25,6 +28,8 @@ import org.openhab.binding.elroconnects.internal.handler.ElroConnectsMotionSenso import org.openhab.binding.elroconnects.internal.handler.ElroConnectsPowerSocketHandler; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsSmokeAlarmHandler; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsWaterAlarmHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.io.net.http.HttpClientInitializationException; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -32,8 +37,12 @@ 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.Deactivate; import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link ElroConnectsHandlerFactory} is responsible for creating things and thing @@ -44,9 +53,37 @@ import org.osgi.service.component.annotations.Reference; @NonNullByDefault @Component(configurationPid = "binding.elroconnects", service = ThingHandlerFactory.class) public class ElroConnectsHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(ElroConnectsHandlerFactory.class); - private @NonNullByDefault({}) NetworkAddressService networkAddressService; - private @NonNullByDefault({}) ElroConnectsDynamicStateDescriptionProvider dynamicStateDescriptionProvider; + private final HttpClientFactory httpClientFactory; + private final NetworkAddressService networkAddressService; + private final ElroConnectsDynamicStateDescriptionProvider dynamicStateDescriptionProvider; + + private @Nullable HttpClient httpClient; + + @Activate + public ElroConnectsHandlerFactory(final @Reference HttpClientFactory httpClientFactory, + final @Reference NetworkAddressService networkAddressService, + final @Reference ElroConnectsDynamicStateDescriptionProvider dynamicStateDescriptionProvider) { + this.httpClientFactory = httpClientFactory; + this.networkAddressService = networkAddressService; + this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider; + } + + @Deactivate + public void deactivate() { + HttpClient client = httpClient; + + if (client != null) { + try { + client.stop(); + } catch (Exception e) { + // catching exception is necessary due to the signature of HttpClient.stop() + logger.debug("Failed to stop http client: {}", e.getMessage()); + } + httpClient = null; + } + } @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -56,6 +93,8 @@ public class ElroConnectsHandlerFactory extends BaseThingHandlerFactory { @Override protected @Nullable ThingHandler createHandler(Thing thing) { switch (thing.getThingTypeUID().getId()) { + case TYPE_ACCOUNT: + return createElroConnectsAccountHandler(thing); case TYPE_CONNECTOR: return new ElroConnectsBridgeHandler((Bridge) thing, networkAddressService, dynamicStateDescriptionProvider); @@ -80,23 +119,27 @@ public class ElroConnectsHandlerFactory extends BaseThingHandlerFactory { } } - @Reference - protected void setNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = networkAddressService; - } + private ThingHandler createElroConnectsAccountHandler(Thing thing) { + // Create and start the httpClient for the first ElroConnectsAccountHandler that gets created. We cannot use the + // common http client because we need to disable the authentication protocol handler. + HttpClient client = httpClient; + if (client == null) { + client = httpClientFactory.createHttpClient(BINDING_ID); + httpClient = client; - protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) { - this.networkAddressService = null; - } + try { + client.start(); - @Reference - protected void setDynamicStateDescriptionProvider( - ElroConnectsDynamicStateDescriptionProvider dynamicStateDescriptionProver) { - this.dynamicStateDescriptionProvider = dynamicStateDescriptionProver; - } - - protected void unsetDynamicStateDescriptionProvider( - ElroConnectsDynamicStateDescriptionProvider dynamicStateDescriptionProver) { - this.dynamicStateDescriptionProvider = null; + // The getAccessToken call in the ElroConnectsAccountHandler returns an invalid 401 response on + // authentication error, missing the www-authentication header. This header should be there according to + // RFC7235. This workaround removes the protocol handler and the check. + client.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME); + } catch (Exception e) { + // catching exception is necessary due to the signature of HttpClient.start() + logger.debug("Failed to start http client: {}", e.getMessage()); + throw new HttpClientInitializationException("Could not initialize HttpClient", e); + } + } + return new ElroConnectsAccountHandler((Bridge) thing, client); } } diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsMessage.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsMessage.java index ac7ddea8760..679d224b6d2 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsMessage.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/ElroConnectsMessage.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.elroconnects.internal; +import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.*; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.elroconnects.internal.util.ElroConnectsUtil; @@ -20,7 +22,8 @@ import com.google.gson.annotations.SerializedName; /** * The {@link ElroConnectsMessage} represents the JSON messages exchanged with the ELRO Connects K1 Connector. This - * class is used to serialize/deserialize the messages. + * class is used to serialize/deserialize the messages. The class also maps cmdId's from older firmware to the newer + * codes and will encode/decode fields that are encoded in the messages with newer firmware versions. * * @author Mark Herwege - Initial contribution */ @@ -28,6 +31,9 @@ import com.google.gson.annotations.SerializedName; @NonNullByDefault public class ElroConnectsMessage { + private transient boolean legacyFirmware = false; // legacy firmware uses different cmd id's and will not encode + // device id when sending messages + private static class Data { private int cmdId; @@ -64,15 +70,16 @@ public class ElroConnectsMessage { private Params params = new Params(); public ElroConnectsMessage(int msgId, String devTid, String ctrlKey, int cmdId) { + this(msgId, devTid, ctrlKey, cmdId, false); + } + + public ElroConnectsMessage(int msgId, String devTid, String ctrlKey, int cmdId, boolean legacyFirmware) { this.msgId = msgId; params.devTid = devTid; params.ctrlKey = ctrlKey; - params.data.cmdId = cmdId; - } + params.data.cmdId = legacyFirmware ? ELRO_LEGACY_MESSAGES.getOrDefault(cmdId, cmdId) : cmdId; - public ElroConnectsMessage(int msgId) { - this.msgId = msgId; - action = "heartbeat"; + this.legacyFirmware = legacyFirmware; } public ElroConnectsMessage withDeviceStatus(String deviceStatus) { @@ -81,17 +88,22 @@ public class ElroConnectsMessage { } public ElroConnectsMessage withDeviceId(int deviceId) { - params.data.deviceId = deviceId; + params.data.deviceId = isLegacy() ? deviceId : ElroConnectsUtil.encode(deviceId); return this; } public ElroConnectsMessage withSceneType(int sceneType) { - params.data.sceneType = sceneType; + params.data.sceneType = isLegacy() ? sceneType : ElroConnectsUtil.encode(sceneType); + return this; + } + + public ElroConnectsMessage withDeviceName(String deviceName) { + params.data.deviceName = deviceName; return this; } public ElroConnectsMessage withSceneGroup(int sceneGroup) { - params.data.sceneGroup = sceneGroup; + params.data.sceneGroup = isLegacy() ? sceneGroup : ElroConnectsUtil.encode(sceneGroup); return this; } @@ -105,6 +117,10 @@ public class ElroConnectsMessage { return this; } + private boolean isLegacy() { + return ELRO_NEW_MESSAGES.containsKey(params.data.cmdId) || legacyFirmware; + } + public int getMsgId() { return msgId; } @@ -114,7 +130,7 @@ public class ElroConnectsMessage { } public int getCmdId() { - return params.data.cmdId; + return ELRO_NEW_MESSAGES.getOrDefault(params.data.cmdId, params.data.cmdId); } public String getDeviceStatus() { @@ -122,11 +138,13 @@ public class ElroConnectsMessage { } public int getSceneGroup() { - return ElroConnectsUtil.intOrZero(params.data.sceneGroup); + int sceneGroup = ElroConnectsUtil.intOrZero(params.data.sceneGroup); + return isLegacy() ? sceneGroup : ElroConnectsUtil.decode(sceneGroup, msgId); } public int getSceneType() { - return ElroConnectsUtil.intOrZero(params.data.sceneType); + int sceneType = ElroConnectsUtil.intOrZero(params.data.sceneType); + return isLegacy() ? sceneType : ElroConnectsUtil.decode(sceneType, msgId); } public String getSceneContent() { @@ -138,7 +156,8 @@ public class ElroConnectsMessage { } public int getDeviceId() { - return ElroConnectsUtil.intOrZero(params.data.deviceId); + int deviceId = ElroConnectsUtil.intOrZero(params.data.deviceId); + return isLegacy() ? deviceId : ElroConnectsUtil.decode(deviceId, msgId); } public String getDeviceName() { diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/console/ElroConnectsCommandExtension.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/console/ElroConnectsCommandExtension.java new file mode 100644 index 00000000000..496e72472c5 --- /dev/null +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/console/ElroConnectsCommandExtension.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.elroconnects.internal.console; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.elroconnects.internal.handler.ElroConnectsBridgeHandler; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link ElroConnectsCommandExtension} is responsible for handling console commands + * + * @author Mark Herwege - Initial contribution + */ + +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class ElroConnectsCommandExtension extends AbstractConsoleCommandExtension { + + private static final String CONNECTORS = "connectors"; + private static final String DEVICES = "devices"; + private static final String REFRESH = "refresh"; + private static final String RENAME = "rename"; + private static final String JOIN = "join"; + private static final String REPLACE = "replace"; + private static final String REMOVE = "remove"; + private static final String CANCEL = "cancel"; + + private final ThingRegistry thingRegistry; + + @Activate + public ElroConnectsCommandExtension(final @Reference ThingRegistry thingRegistry) { + super("elroconnects", "Interact with the ELRO Connects binding"); + this.thingRegistry = thingRegistry; + } + + @Override + public void execute(String[] args, Console console) { + if ((args.length < 1) || (args.length > 4)) { + console.println("Invalid number of arguments"); + printUsage(console); + return; + } + + List bridgeHandlers = thingRegistry.getAll().stream() + .filter(t -> t.getHandler() instanceof ElroConnectsBridgeHandler) + .map(b -> ((ElroConnectsBridgeHandler) b.getHandler())).collect(Collectors.toList()); + + if (CONNECTORS.equals(args[0])) { + if (args.length > 1) { + console.println("No extra argument allowed after 'connectors'"); + printUsage(console); + } else if (bridgeHandlers.isEmpty()) { + console.println("No K1 hubs added as a bridge"); + } else { + bridgeHandlers.forEach(b -> console.printf("%s%n", b.getConnectorId())); + } + return; + } + + Optional bridgeOptional = bridgeHandlers.stream() + .filter(b -> b.getConnectorId().equals(args[0])).findAny(); + if (bridgeOptional.isEmpty()) { + console.println("'" + args[0] + "' is not a valid connectorId for an ELRO Connects bridge"); + printUsage(console); + return; + } + ElroConnectsBridgeHandler bridgeHandler = bridgeOptional.get(); + if (!ThingStatus.ONLINE.equals(bridgeHandler.getThing().getStatus())) { + console.println("ELRO Connects bridge not online, no commands allowed"); + return; + } + + if (args.length < 2) { + console.println("Invalid number of arguments"); + printUsage(console); + return; + } + + switch (args[1]) { + case REFRESH: + if (args.length > 2) { + console.println("No extra argument allowed after '" + args[1] + "'"); + printUsage(console); + } else { + bridgeHandler.refreshFromConsole(); + } + break; + case DEVICES: + if (args.length > 2) { + console.println("No extra argument allowed after '" + args[1] + "'"); + printUsage(console); + } else { + bridgeHandler.listDevicesFromConsole().forEach((id, name) -> console.printf("%5d %s%n", id, name)); + } + break; + case JOIN: + if (args.length > 2) { + console.println("No extra argument allowed after '" + args[1] + "'"); + printUsage(console); + } else { + bridgeHandler.joinDeviceFromConsole(); + console.println("Device join mode active"); + } + break; + case CANCEL: + if (args.length < 3) { + console.println("Invalid number of arguments"); + printUsage(console); + } else if (JOIN.equals(args[2]) || REPLACE.equals(args[2])) { + if (args.length > 3) { + console.println("No extra argument allowed after '" + args[2] + "'"); + printUsage(console); + } else { + bridgeHandler.cancelJoinDeviceFromConsole(); + console.println("Device join mode inactive"); + } + } else { + console.println("Command argument '" + args[2] + "' not recognized"); + printUsage(console); + return; + } + break; + case REPLACE: + if (args.length < 3) { + console.println("Invalid number of arguments"); + printUsage(console); + } else { + try { + if (args.length > 3) { + console.println("No extra argument allowed after '" + args[2] + "'"); + printUsage(console); + } else if (!bridgeHandler.replaceDeviceFromConsole(Integer.valueOf(args[2]))) { + console.println("Command argument '" + args[2] + "' is not a known deviceId"); + printUsage(console); + } else { + console.println("Device join mode active"); + } + } catch (NumberFormatException e) { + console.println("Command argument '" + args[2] + "' is not a numeric deviceId"); + printUsage(console); + } + } + break; + case REMOVE: + if (args.length < 3) { + console.println("Invalid number of arguments"); + printUsage(console); + } else { + try { + if (args.length > 3) { + console.println("No extra argument allowed after '" + args[2] + "'"); + printUsage(console); + } else if (!bridgeHandler.removeDeviceFromConsole(Integer.valueOf(args[2]))) { + console.println("Command argument '" + args[2] + "' is not a known deviceId"); + printUsage(console); + } + } catch (NumberFormatException e) { + console.println("Command argument '" + args[2] + "' is not a numeric deviceId"); + printUsage(console); + } + } + break; + case RENAME: + if (args.length < 4) { + console.println("Invalid number of arguments"); + printUsage(console); + } else { + try { + if (args.length > 4) { + console.println("No extra argument allowed after '" + args[2] + " " + args[3] + "'"); + printUsage(console); + } else if (!bridgeHandler.renameDeviceFromConsole(Integer.valueOf(args[2]), args[3])) { + console.println("Command argument '" + args[2] + "' is not a known deviceId"); + printUsage(console); + } + } catch (NumberFormatException e) { + console.println("Command argument '" + args[2] + "' is not a numeric deviceId"); + printUsage(console); + } + } + break; + default: + console.println("Command argument '" + args[1] + "' not recognized"); + printUsage(console); + } + } + + public void joinDeviceCancelled(Console console) { + console.println("Device join mode inactive"); + } + + @Override + public List getUsages() { + return Arrays.asList(new String[] { buildCommandUsage(CONNECTORS, "list all K1 hub connector ID's"), + buildCommandUsage(" " + REFRESH, "refresh device list, names and status"), + buildCommandUsage(" " + DEVICES, "list all devices connected to the K1 hub"), + buildCommandUsage(" " + RENAME + " ", "rename device with ID"), + buildCommandUsage(" " + JOIN, + "put K1 hub in device join mode, 3 short presses on the device will join it to the hub"), + buildCommandUsage(" " + CANCEL + " " + JOIN, "cancel K1 hub device join mode"), + buildCommandUsage(" " + REPLACE + " ", + "replace device with ID by newly joined device, puts K1 hub in join mode"), + buildCommandUsage(" " + CANCEL + " " + REPLACE, "cancel K1 hub device replace mode"), + buildCommandUsage(" " + REMOVE + " ", "remove device with ID from K1 hub") }); + } +} diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsConnector.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsConnector.java new file mode 100644 index 00000000000..872d5c2c1e0 --- /dev/null +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsConnector.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.elroconnects.internal.devices; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ElroConnectsConnector} class represents a device response received from the ELRO Connects + * cloud. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +public class ElroConnectsConnector { + + String devTid = ""; + public String ctrlKey = ""; + public String binVersion = ""; + public String binType = ""; + public String sdkVer = ""; + public String model = ""; + public boolean online; + public String desc = ""; + + public String getDevTid() { + return devTid; + } +} diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevice.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevice.java index c5a3bf32563..cbda685904f 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevice.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevice.java @@ -12,13 +12,20 @@ */ package org.openhab.binding.elroconnects.internal.devices; +import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.*; + +import java.io.IOException; import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.ElroDeviceStatus; +import org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.ElroDeviceType; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsBridgeHandler; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsDeviceHandler; +import org.openhab.binding.elroconnects.internal.util.ElroConnectsUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link ElroConnectsDevice} is an abstract class representing all basic properties for ELRO Connects devices. @@ -29,6 +36,8 @@ import org.openhab.binding.elroconnects.internal.handler.ElroConnectsDeviceHandl @NonNullByDefault public abstract class ElroConnectsDevice { + private final Logger logger = LoggerFactory.getLogger(ElroConnectsDevice.class); + // minimum data to create an instance of the class protected int deviceId; protected ElroConnectsBridgeHandler bridge; @@ -72,6 +81,17 @@ public abstract class ElroConnectsDevice { this.deviceName = deviceName; } + public void updateDeviceName(String deviceName) { + try { + if (!ElroConnectsUtil.equals(getDeviceName(), deviceName, 15)) { + bridge.renameDevice(deviceId, deviceName); + setDeviceName(deviceName); + } + } catch (IOException e) { + logger.debug("Failed to update device name: {}", e.getMessage()); + } + } + public void setDeviceType(String deviceType) { this.deviceType = deviceType; } @@ -81,7 +101,16 @@ public abstract class ElroConnectsDevice { } public String getDeviceName() { - return deviceName; + String typeName = null; + ElroDeviceType type = TYPE_MAP.get(getDeviceType()); + if (type != null) { + typeName = TYPE_NAMES.get(type); + } + if (typeName == null) { + typeName = getDeviceType(); + } + + return deviceName.isEmpty() ? typeName + "-" + String.valueOf(deviceId) : deviceName; } public String getDeviceType() { diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceCxsmAlarm.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceCxsmAlarm.java index dcaec16879d..70beffc4ec5 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceCxsmAlarm.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceCxsmAlarm.java @@ -106,9 +106,12 @@ public class ElroConnectsDeviceCxsmAlarm extends ElroConnectsDevice { } ElroDeviceStatus elroStatus = getStatus(); + int signalStrength = 0; int batteryLevel = 0; String deviceStatus = this.deviceStatus; if (deviceStatus.length() >= 6) { + signalStrength = Integer.parseInt(deviceStatus.substring(0, 2), 16); + signalStrength = (signalStrength > 4) ? 4 : ((signalStrength < 0) ? 0 : signalStrength); batteryLevel = Integer.parseInt(deviceStatus.substring(2, 4), 16); } else { elroStatus = ElroDeviceStatus.FAULT; @@ -117,17 +120,20 @@ public class ElroConnectsDeviceCxsmAlarm extends ElroConnectsDevice { switch (elroStatus) { case UNDEF: + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device " + deviceId + " is not syncing with K1 hub"); break; case FAULT: + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Device " + deviceId + " has a fault"); break; default: + handler.updateState(SIGNAL_STRENGTH, new DecimalType(signalStrength)); handler.updateState(BATTERY_LEVEL, new DecimalType(batteryLevel)); handler.updateState(LOW_BATTERY, (batteryLevel < 15) ? OnOffType.ON : OnOffType.OFF); handler.updateStatus(ThingStatus.ONLINE); diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceEntrySensor.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceEntrySensor.java index 89be3e9629a..4ac00ee7d0f 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceEntrySensor.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceEntrySensor.java @@ -70,9 +70,12 @@ public class ElroConnectsDeviceEntrySensor extends ElroConnectsDevice { } ElroDeviceStatus elroStatus = getStatus(); + int signalStrength = 0; int batteryLevel = 0; String deviceStatus = this.deviceStatus; if (deviceStatus.length() >= 6) { + signalStrength = Integer.parseInt(deviceStatus.substring(0, 2), 16); + signalStrength = (signalStrength > 4) ? 4 : ((signalStrength < 0) ? 0 : signalStrength); batteryLevel = Integer.parseInt(deviceStatus.substring(2, 4), 16); } else { elroStatus = ElroDeviceStatus.FAULT; @@ -82,6 +85,7 @@ public class ElroConnectsDeviceEntrySensor extends ElroConnectsDevice { switch (elroStatus) { case UNDEF: handler.updateState(ENTRY, UnDefType.UNDEF); + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -89,6 +93,7 @@ public class ElroConnectsDeviceEntrySensor extends ElroConnectsDevice { break; case FAULT: handler.updateState(ENTRY, UnDefType.UNDEF); + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Device " + deviceId + " has a fault"); @@ -96,6 +101,7 @@ public class ElroConnectsDeviceEntrySensor extends ElroConnectsDevice { default: handler.updateState(ENTRY, ElroDeviceStatus.OPEN.equals(elroStatus) ? OpenClosedType.OPEN : OpenClosedType.CLOSED); + handler.updateState(SIGNAL_STRENGTH, new DecimalType(signalStrength)); handler.updateState(BATTERY_LEVEL, new DecimalType(batteryLevel)); handler.updateState(LOW_BATTERY, (batteryLevel < 15) ? OnOffType.ON : OnOffType.OFF); handler.updateStatus(ThingStatus.ONLINE); diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceGenericAlarm.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceGenericAlarm.java index 874459918fa..b0edcefdca8 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceGenericAlarm.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceGenericAlarm.java @@ -99,9 +99,12 @@ public class ElroConnectsDeviceGenericAlarm extends ElroConnectsDevice { } ElroDeviceStatus elroStatus = getStatus(); + int signalStrength = 0; int batteryLevel = 0; String deviceStatus = this.deviceStatus; if (deviceStatus.length() >= 6) { + signalStrength = Integer.parseInt(deviceStatus.substring(0, 2), 16); + signalStrength = (signalStrength > 4) ? 4 : ((signalStrength < 0) ? 0 : signalStrength); batteryLevel = Integer.parseInt(deviceStatus.substring(2, 4), 16); } else { elroStatus = ElroDeviceStatus.FAULT; @@ -110,17 +113,20 @@ public class ElroConnectsDeviceGenericAlarm extends ElroConnectsDevice { switch (elroStatus) { case UNDEF: + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device " + deviceId + " is not syncing with K1 hub"); break; case FAULT: + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Device " + deviceId + " has a fault"); break; default: + handler.updateState(SIGNAL_STRENGTH, new DecimalType(signalStrength)); handler.updateState(BATTERY_LEVEL, new DecimalType(batteryLevel)); handler.updateState(LOW_BATTERY, (batteryLevel < 15) ? OnOffType.ON : OnOffType.OFF); handler.updateStatus(ThingStatus.ONLINE); diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceMotionSensor.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceMotionSensor.java index 6efcc016eb6..be7d92c915e 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceMotionSensor.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceMotionSensor.java @@ -69,9 +69,12 @@ public class ElroConnectsDeviceMotionSensor extends ElroConnectsDevice { } ElroDeviceStatus elroStatus = getStatus(); + int signalStrength = 0; int batteryLevel = 0; String deviceStatus = this.deviceStatus; if (deviceStatus.length() >= 6) { + signalStrength = Integer.parseInt(deviceStatus.substring(0, 2), 16); + signalStrength = (signalStrength > 4) ? 4 : ((signalStrength < 0) ? 0 : signalStrength); batteryLevel = Integer.parseInt(deviceStatus.substring(2, 4), 16); } else { elroStatus = ElroDeviceStatus.FAULT; @@ -81,6 +84,7 @@ public class ElroConnectsDeviceMotionSensor extends ElroConnectsDevice { switch (elroStatus) { case UNDEF: handler.updateState(MOTION, UnDefType.UNDEF); + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -88,6 +92,7 @@ public class ElroConnectsDeviceMotionSensor extends ElroConnectsDevice { break; case FAULT: handler.updateState(MOTION, UnDefType.UNDEF); + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Device " + deviceId + " has a fault"); @@ -95,6 +100,7 @@ public class ElroConnectsDeviceMotionSensor extends ElroConnectsDevice { default: handler.updateState(MOTION, ElroDeviceStatus.TRIGGERED.equals(elroStatus) ? OnOffType.ON : OnOffType.OFF); + handler.updateState(SIGNAL_STRENGTH, new DecimalType(signalStrength)); handler.updateState(BATTERY_LEVEL, new DecimalType(batteryLevel)); handler.updateState(LOW_BATTERY, (batteryLevel < 15) ? OnOffType.ON : OnOffType.OFF); handler.updateStatus(ThingStatus.ONLINE); diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevicePowerSocket.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevicePowerSocket.java index e64b84add09..2683fb688dc 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevicePowerSocket.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDevicePowerSocket.java @@ -12,13 +12,14 @@ */ package org.openhab.binding.elroconnects.internal.devices; -import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.POWER_STATE; +import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.*; import java.io.IOException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsBridgeHandler; import org.openhab.binding.elroconnects.internal.handler.ElroConnectsDeviceHandler; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; @@ -71,6 +72,10 @@ public class ElroConnectsDevicePowerSocket extends ElroConnectsDevice { return; } + int signalStrength = Integer.parseInt(deviceStatus.substring(0, 2), 16); + signalStrength = (signalStrength > 4) ? 4 : ((signalStrength < 0) ? 0 : signalStrength); + handler.updateState(SIGNAL_STRENGTH, new DecimalType(signalStrength)); + String status = deviceStatus.substring(4, 6); State state = STAT_ON.equals(status) ? OnOffType.ON : (STAT_OFF.equals(status) ? OnOffType.OFF : UnDefType.UNDEF); diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceTemperatureSensor.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceTemperatureSensor.java index 34088f0b31d..76314e2ab42 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceTemperatureSensor.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/devices/ElroConnectsDeviceTemperatureSensor.java @@ -52,11 +52,14 @@ public class ElroConnectsDeviceTemperatureSensor extends ElroConnectsDevice { } ElroDeviceStatus elroStatus = ElroDeviceStatus.NORMAL; + int signalStrength = 0; int batteryLevel = 0; int temperature = 0; int humidity = 0; String deviceStatus = this.deviceStatus; if (deviceStatus.length() >= 8) { + signalStrength = Integer.parseInt(deviceStatus.substring(0, 2), 16); + signalStrength = (signalStrength > 4) ? 4 : ((signalStrength < 0) ? 0 : signalStrength); batteryLevel = Integer.parseInt(deviceStatus.substring(2, 4), 16); temperature = Byte.parseByte(deviceStatus.substring(4, 6), 16); humidity = Integer.parseInt(deviceStatus.substring(6, 8)); @@ -67,6 +70,7 @@ public class ElroConnectsDeviceTemperatureSensor extends ElroConnectsDevice { switch (elroStatus) { case FAULT: + handler.updateState(SIGNAL_STRENGTH, UnDefType.UNDEF); handler.updateState(BATTERY_LEVEL, UnDefType.UNDEF); handler.updateState(LOW_BATTERY, UnDefType.UNDEF); handler.updateState(TEMPERATURE, UnDefType.UNDEF); @@ -74,6 +78,7 @@ public class ElroConnectsDeviceTemperatureSensor extends ElroConnectsDevice { handler.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Device " + deviceId + " has a fault"); break; default: + handler.updateState(SIGNAL_STRENGTH, new DecimalType(signalStrength)); handler.updateState(BATTERY_LEVEL, new DecimalType(batteryLevel)); handler.updateState(LOW_BATTERY, (batteryLevel < 15) ? OnOffType.ON : OnOffType.OFF); handler.updateState(TEMPERATURE, new QuantityType<>(temperature, CELSIUS)); diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/discovery/ElroConnectsBridgeDiscoveryService.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/discovery/ElroConnectsBridgeDiscoveryService.java new file mode 100644 index 00000000000..c1206c3ace1 --- /dev/null +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/discovery/ElroConnectsBridgeDiscoveryService.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.elroconnects.internal.discovery; + +import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.*; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants; +import org.openhab.binding.elroconnects.internal.devices.ElroConnectsConnector; +import org.openhab.binding.elroconnects.internal.handler.ElroConnectsAccountHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +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; + +/** + * {@link ElroConnectsBridgeDiscoveryService} is used to discover K1 Hubs attached to an ELRO Connects cloud account. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public class ElroConnectsBridgeDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(ElroConnectsBridgeDiscoveryService.class); + + private @Nullable ElroConnectsAccountHandler accountHandler; + + private volatile @Nullable ScheduledFuture discoveryJob; + + private static final int TIMEOUT_S = 5; + private static final int INITIAL_DELAY_S = 5; // initial delay for polling to allow time for login and retrieval in + // ElroConnectsAccountHandler to complete + private static final int REFRESH_INTERVAL_S = 60; + + public ElroConnectsBridgeDiscoveryService() { + super(ElroConnectsBindingConstants.SUPPORTED_CONNECTOR_TYPES_UIDS, TIMEOUT_S); + logger.debug("Bridge discovery service started"); + } + + @Override + protected void startScan() { + scheduler.execute(this::discoverConnectors); // If background account polling is not enabled for the handler, + // this will trigger http requests, therefore do in separate thread + // to be able to return quickly + } + + private void discoverConnectors() { + logger.debug("Starting hub discovery scan"); + ElroConnectsAccountHandler account = accountHandler; + if (account == null) { + return; + } + + ThingUID bridgeUID = account.getThing().getUID(); + + Map connectors = account.getDevices(); + if (connectors == null) { + return; + } + + connectors.entrySet().forEach(c -> { + if (c.getValue().online) { + String connectorId = c.getKey(); + String firmwareVersion = c.getValue().binVersion; + boolean legacy = false; + try { + legacy = !(Integer.valueOf(firmwareVersion.substring(firmwareVersion.lastIndexOf(".") + 1)) > 14); + } catch (NumberFormatException e) { + // Assume new firmware if we cannot decode firmwareVersion + logger.debug("Cannot get firmware version from {}, assume new firmware", firmwareVersion); + } + final Map properties = new HashMap<>(); + properties.put(CONFIG_CONNECTOR_ID, connectorId); + properties.put(CONFIG_LEGACY_FIRMWARE, legacy); + properties.put("binVersion", c.getValue().binVersion); + properties.put("binType", c.getValue().binType); + properties.put("sdkVer", c.getValue().sdkVer); + properties.put(Thing.PROPERTY_MODEL_ID, c.getValue().model); + properties.put("desc", c.getValue().desc); + + thingDiscovered( + DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_CONNECTOR, bridgeUID, connectorId)) + .withLabel(c.getValue().desc).withBridge(bridgeUID).withProperties(properties) + .withRepresentationProperty(CONFIG_CONNECTOR_ID).build()); + } + }); + } + + @Override + protected synchronized void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan()); + } + + @Override + public void startBackgroundDiscovery() { + logger.debug("Start bridge background discovery"); + ScheduledFuture job = discoveryJob; + if (job == null || job.isCancelled()) { + discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverConnectors, INITIAL_DELAY_S, + REFRESH_INTERVAL_S, TimeUnit.SECONDS); + } + } + + @Override + public void stopBackgroundDiscovery() { + logger.debug("Stop bridge background discovery"); + ScheduledFuture job = discoveryJob; + if (job != null && !job.isCancelled()) { + job.cancel(true); + discoveryJob = null; + } + } + + @Override + public void deactivate() { + removeOlderResults(Instant.now().toEpochMilli()); + super.deactivate(); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + ElroConnectsAccountHandler account = null; + if (handler instanceof ElroConnectsAccountHandler) { + account = (ElroConnectsAccountHandler) handler; + accountHandler = account; + } + + if (account != null) { + account.setDiscoveryService(this); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return accountHandler; + } +} diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/discovery/ElroConnectsDiscoveryService.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/discovery/ElroConnectsDiscoveryService.java index a1eb4f2d08b..e09fdba9f19 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/discovery/ElroConnectsDiscoveryService.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/discovery/ElroConnectsDiscoveryService.java @@ -14,6 +14,7 @@ package org.openhab.binding.elroconnects.internal.discovery; import static org.openhab.binding.elroconnects.internal.ElroConnectsBindingConstants.*; +import java.time.Instant; import java.util.Map; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -45,14 +46,14 @@ public class ElroConnectsDiscoveryService extends AbstractDiscoveryService imple private @Nullable ElroConnectsBridgeHandler bridgeHandler; - private static final int TIMEOUT_SECONDS = 5; - private static final int REFRESH_INTERVAL_SECONDS = 60; + private static final int TIMEOUT_S = 5; + private static final int REFRESH_INTERVAL_S = 60; private @Nullable ScheduledFuture discoveryJob; public ElroConnectsDiscoveryService() { - super(ElroConnectsBindingConstants.SUPPORTED_THING_TYPES_UIDS, TIMEOUT_SECONDS); - logger.debug("Bridge discovery service started"); + super(ElroConnectsBindingConstants.SUPPORTED_DEVICE_TYPES_UIDS, TIMEOUT_S); + logger.debug("Discovery service started"); } @Override @@ -93,11 +94,11 @@ public class ElroConnectsDiscoveryService extends AbstractDiscoveryService imple } @Override - protected void startBackgroundDiscovery() { + public void startBackgroundDiscovery() { logger.debug("Start device background discovery"); ScheduledFuture job = discoveryJob; if (job == null || job.isCancelled()) { - discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, REFRESH_INTERVAL_SECONDS, + discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, REFRESH_INTERVAL_S, TimeUnit.SECONDS); } } @@ -106,7 +107,7 @@ public class ElroConnectsDiscoveryService extends AbstractDiscoveryService imple protected void stopBackgroundDiscovery() { logger.debug("Stop device background discovery"); ScheduledFuture job = discoveryJob; - if (job != null) { + if (job != null && !job.isCancelled()) { job.cancel(true); discoveryJob = null; } @@ -114,13 +115,20 @@ public class ElroConnectsDiscoveryService extends AbstractDiscoveryService imple @Override public void deactivate() { + removeOlderResults(Instant.now().toEpochMilli()); super.deactivate(); } @Override public void setThingHandler(@Nullable ThingHandler handler) { + ElroConnectsBridgeHandler bridge = null; if (handler instanceof ElroConnectsBridgeHandler) { - bridgeHandler = (ElroConnectsBridgeHandler) handler; + bridge = (ElroConnectsBridgeHandler) handler; + bridgeHandler = bridge; + } + + if (bridge != null) { + bridge.setDiscoveryService(this); } } diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountConfiguration.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountConfiguration.java new file mode 100644 index 00000000000..2bb8f65eced --- /dev/null +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.elroconnects.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link ElroConnectsAccountConfiguration} class contains fields mapping bridge configuration parameters. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +public class ElroConnectsAccountConfiguration { + + public @Nullable String username; + public @Nullable String password; + public boolean enableBackgroundDiscovery = true; +} diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountException.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountException.java new file mode 100644 index 00000000000..69bd61a7a0d --- /dev/null +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.elroconnects.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ElroConnectsAccountException} class is the exception for all cloud account errors. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +class ElroConnectsAccountException extends Exception { + + private static final long serialVersionUID = -1038059604759958044L; + + public ElroConnectsAccountException(String message) { + super(message); + } + + public ElroConnectsAccountException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountHandler.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountHandler.java new file mode 100644 index 00000000000..a9aeda36edc --- /dev/null +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsAccountHandler.java @@ -0,0 +1,371 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.elroconnects.internal.handler; + +import java.lang.reflect.Type; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +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.HttpResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.elroconnects.internal.devices.ElroConnectsConnector; +import org.openhab.binding.elroconnects.internal.discovery.ElroConnectsBridgeDiscoveryService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link ElroConnectsAccountHandler} is the bridge handler responsible to for connecting the the ELRO Connects + * cloud service and retrieving the defined K1 hubs. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +public class ElroConnectsAccountHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(ElroConnectsAccountHandler.class); + + private static final String ELRO_CLOUD_LOGIN_URL = "https://uaa-openapi.hekreu.me/login"; + private static final String ELRO_CLOUD_URL = "https://user-openapi.hekreu.me/device"; + private static final String ELRO_PID = "01288154146"; // ELRO Connects Enterprise PID on hekr cloud + + private static final int TIMEOUT_MS = 2500; + private static final int REFRESH_INTERVAL_S = 300; + + private boolean enableBackgroundDiscovery = true; + + private volatile @Nullable ScheduledFuture pollingJob; + private final HttpClient client; + + private Gson gson = new Gson(); + private Type loginType = new TypeToken>() { + }.getType(); + private Type deviceListType = new TypeToken>() { + }.getType(); + + private final Map login = new HashMap<>(); + private String loginJson = ""; + private volatile @Nullable String accessToken; + + private volatile boolean retry = false; // Flag for retrying login when token expired during poll, prevents multiple + // recursive retries + + private volatile Map devices = new HashMap<>(); + + private @Nullable ElroConnectsBridgeDiscoveryService discoveryService = null; + + public ElroConnectsAccountHandler(Bridge bridge, HttpClient client) { + super(bridge); + this.client = client; + login.put("pid", ELRO_PID); + login.put("clientType", "WEB"); + } + + @Override + public void initialize() { + accessToken = null; + + ElroConnectsAccountConfiguration config = getConfigAs(ElroConnectsAccountConfiguration.class); + String username = config.username; + String password = config.password; + enableBackgroundDiscovery = config.enableBackgroundDiscovery; + + if ((username == null) || username.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "@text/offline.no-username"); + return; + } + if ((password == null) || password.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "@text/offline.no-password"); + return; + } + + login.put("username", username); + login.put("password", password); + loginJson = gson.toJson(login); + + // If background discovery is enabled, start polling (will do login first), else only login to take the thing + // online if successful + if (enableBackgroundDiscovery) { + startPolling(); + } else { + scheduler.execute(this::login); + } + } + + @Override + public void dispose() { + logger.debug("Handler disposed"); + stopPolling(); + super.dispose(); + } + + private void startPolling() { + final ScheduledFuture localRefreshJob = this.pollingJob; + if (localRefreshJob == null || localRefreshJob.isCancelled()) { + logger.debug("Start polling"); + pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, REFRESH_INTERVAL_S, TimeUnit.SECONDS); + } + + ElroConnectsBridgeDiscoveryService service = this.discoveryService; + if (service != null) { + service.startBackgroundDiscovery(); + } + } + + private void stopPolling() { + ElroConnectsBridgeDiscoveryService service = this.discoveryService; + if (service != null) { + service.stopBackgroundDiscovery(); + } + + final ScheduledFuture localPollingJob = this.pollingJob; + if (localPollingJob != null && !localPollingJob.isCancelled()) { + logger.debug("Stop polling"); + localPollingJob.cancel(true); + pollingJob = null; + } + } + + private void poll() { + logger.debug("Polling"); + + // when access token not yet received or expired, try to login first + if (accessToken == null) { + login(); + } + if (accessToken == null) { + return; + } + + try { + getControllers().handle((devicesList, accountException) -> { + if (devicesList != null) { + logger.trace("deviceList response: {}", devicesList); + + List response = null; + try { + response = gson.fromJson(devicesList, deviceListType); + } catch (JsonParseException parseException) { + logger.warn("Parsing failed: {}", parseException.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.request-failed"); + return null; + } + Map devices = new HashMap<>(); + if (response != null) { + response.forEach(d -> devices.put(d.getDevTid(), d)); + } + this.devices = devices; + updateStatus(ThingStatus.ONLINE); + } else { + if (accountException == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + accountException.getLocalizedMessage()); + } + } + + return null; + }).get(); + } catch (InterruptedException e) { + updateStatus(ThingStatus.OFFLINE); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.debug("Poll exception", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + } + + private void login() { + logger.debug("Login"); + try { + getAccessToken().handle((accessToken, accountException) -> { + this.accessToken = accessToken; + if (accessToken != null) { + updateStatus(ThingStatus.ONLINE); + } else { + if (accountException == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + accountException.getLocalizedMessage()); + } + } + + return null; + }).get(); + } catch (InterruptedException e) { + updateStatus(ThingStatus.OFFLINE); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.debug("Login exception", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + } + + private CompletableFuture<@Nullable String> getAccessToken() { + CompletableFuture<@Nullable String> f = new CompletableFuture<>(); + Request request = client.newRequest(URI.create(ELRO_CLOUD_LOGIN_URL)); + + request.method(HttpMethod.POST).content(new StringContentProvider(loginJson), "application/json") + .timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() { + @NonNullByDefault({}) + @Override + public void onComplete(Result result) { + if (result.isSucceeded()) { + final HttpResponse response = (HttpResponse) result.getResponse(); + if (response.getStatus() == 200) { + try { + Map content = gson.fromJson(getContentAsString(), loginType); + String accessToken = (content != null) ? content.get("access_token") : null; + f.complete(accessToken); + } catch (JsonParseException parseException) { + logger.warn("Access token request response parsing failed: {}", + parseException.getMessage()); + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-failed")); + } + } else if (response.getStatus() == 401) { + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.credentials-error")); + } else { + logger.warn("Unexpected response on access token request: {} - {}", + response.getStatus(), getContentAsString()); + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-failed")); + } + } else { + Throwable e = result.getFailure(); + if (e instanceof SocketTimeoutException || e instanceof TimeoutException) { + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-timeout", e)); + } else { + logger.warn("Access token request failed", e); + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-failed", e)); + } + } + } + }); + + return f; + } + + private CompletableFuture<@Nullable String> getControllers() { + CompletableFuture<@Nullable String> f = new CompletableFuture<>(); + Request request = client.newRequest(URI.create(ELRO_CLOUD_URL)); + + request.method(HttpMethod.GET).header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken) + .header(HttpHeader.ACCEPT, "application/json").timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS) + .send(new BufferingResponseListener() { + @NonNullByDefault({}) + @Override + public void onComplete(Result result) { + if (result.isSucceeded()) { + final HttpResponse response = (HttpResponse) result.getResponse(); + if (response.getStatus() == 200) { + f.complete(getContentAsString()); + } else if (response.getStatus() == 401) { + // Access token expired, so clear it and do a poll that will now start with a login + accessToken = null; + if (!retry) { // Only retry once to avoid infinite recursive loop if no valid token is + // received + retry = true; + logger.debug("Access token expired, retry"); + poll(); + if (accessToken == null) { + logger.debug("Request for new token failed"); + } + } else { + logger.warn("Unexpected response after getting new token : {} - {}", + response.getStatus(), getContentAsString()); + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-failed")); + } + } else { + logger.warn("Unexpected response on get controllers request: {} - {}", + response.getStatus(), getContentAsString()); + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-failed")); + } + } else { + Throwable e = result.getFailure(); + if (e instanceof SocketTimeoutException || e instanceof TimeoutException) { + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-timeout", e)); + } else { + logger.warn("Get controllers request failed", e); + f.completeExceptionally( + new ElroConnectsAccountException("@text/offline.request-failed", e)); + } + } + retry = false; + } + }); + + return f; + } + + @Override + public Collection> getServices() { + return Collections.singleton(ElroConnectsBridgeDiscoveryService.class); + } + + /** + * @return connectors on the account from the ELRO Connects cloud API + */ + public @Nullable Map getDevices() { + if (!enableBackgroundDiscovery) { + poll(); + } + return devices; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // nothing to do, there are no channels + } + + public void setDiscoveryService(ElroConnectsBridgeDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } +} diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeConfiguration.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeConfiguration.java index d588290f1a4..c1b0c73b38f 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeConfiguration.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeConfiguration.java @@ -25,4 +25,5 @@ public class ElroConnectsBridgeConfiguration { public String connectorId = ""; public String ipAddress = ""; public int refreshInterval = 60; + public boolean legacyFirmware = false; } diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeHandler.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeHandler.java index f5d0dddf8ec..b1fa7d676e0 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeHandler.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsBridgeHandler.java @@ -34,6 +34,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -50,7 +51,7 @@ import org.openhab.binding.elroconnects.internal.devices.ElroConnectsDeviceTempe import org.openhab.binding.elroconnects.internal.discovery.ElroConnectsDiscoveryService; import org.openhab.binding.elroconnects.internal.util.ElroConnectsUtil; import org.openhab.core.common.NamedThreadFactory; -import org.openhab.core.library.types.DecimalType; +import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.StringType; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.Bridge; @@ -59,13 +60,13 @@ import org.openhab.core.thing.ChannelUID; 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.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.StateDescription; import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.StateOption; -import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -122,6 +123,8 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { private volatile @Nullable InetAddress addr; private volatile String ctrlKey = ""; + private boolean legacyFirmware = false; + private volatile @Nullable DatagramSocket socket; private volatile @Nullable DatagramPacket ackPacket; @@ -140,6 +143,8 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { private final Gson gsonOut = new Gson(); private Gson gsonIn = new Gson(); + private @Nullable ElroConnectsDiscoveryService discoveryService = null; + public ElroConnectsBridgeHandler(Bridge bridge, NetworkAddressService networkAddressService, ElroConnectsDynamicStateDescriptionProvider stateDescriptionProvider) { super(bridge); @@ -154,13 +159,14 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class); connectorId = config.connectorId; refreshInterval = config.refreshInterval; + legacyFirmware = config.legacyFirmware; if (connectorId.isEmpty()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device ID not set"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.no-device-id"); return; } else if (!CONNECTOR_ID_PATTERN.matcher(connectorId).matches()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Device ID not of format ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC address of the connector"); + String msg = String.format("@text/offline.invalid-device-id [ \"%s\" ]", connectorId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg); return; } @@ -176,38 +182,47 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { private synchronized void startCommunication() { ElroConnectsBridgeConfiguration config = getConfigAs(ElroConnectsBridgeConfiguration.class); - InetAddress addr = this.addr; + InetAddress addr = null; + // First try with configured IP address if there is one String ipAddress = config.ipAddress; if (!ipAddress.isEmpty()) { try { - addr = InetAddress.getByName(ipAddress); - this.addr = addr; - } catch (UnknownHostException e) { - addr = null; + this.addr = InetAddress.getByName(ipAddress); + addr = getAddr(false); + } catch (IOException e) { logger.warn("Unknown host for {}, trying to discover address", ipAddress); } } - try { - addr = getAddr(addr == null); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Error trying to find IP address for connector ID " + connectorId + "."); - stopCommunication(); - return; - } + // Then try broadcast to detect IP address if configured IP address did not work if (addr == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Error trying to find IP address for connector ID " + connectorId + "."); + try { + addr = getAddr(true); + } catch (IOException e) { + String msg = String.format("@text/offline.find-ip-fail [ \"%s\" ]", connectorId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg); + stopCommunication(); + return; + } + } + + if (addr == null) { + String msg = String.format("@text/offline.find-ip-fail [ \"%s\" ]", connectorId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg); stopCommunication(); return; } + // Found valid IP address, update configuration with detected IP address + Configuration configuration = thing.getConfiguration(); + configuration.put(CONFIG_IP_ADDRESS, addr.getHostAddress()); + updateConfiguration(configuration); + String ctrlKey = this.ctrlKey; if (ctrlKey.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Communication data error while starting communication."); + "@text/offline.communication-data-error"); stopCommunication(); return; } @@ -217,8 +232,8 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { socket = createSocket(false); this.socket = socket; } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Socket error while starting communication: " + e.getMessage()); + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg); stopCommunication(); return; } @@ -243,9 +258,15 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { getCurrentScene(); updateStatus(ThingStatus.ONLINE); - updateState(SCENE, new StringType(String.valueOf(currentScene))); + + // Enable discovery of devices + ElroConnectsDiscoveryService service = discoveryService; + if (service != null) { + service.startBackgroundDiscovery(); + } } catch (IOException e) { - restartCommunication("Error in communication getting initial data: " + e.getMessage()); + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); return; } @@ -254,7 +275,7 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { /** * Get the IP address and ctrl key of the connector by broadcasting message with connectorId. This should be used - * when initializing the connection. the ctrlKey field is set. + * when initializing the connection. the ctrlKey and addr fields are set. * * @param broadcast, if true find address by broadcast, otherwise simply send to configured address to retrieve key * only @@ -285,7 +306,7 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { awaitResponse(true); send(socket, queryString, false); } else { - restartCommunication("Error in communication, no socket to send keep alive"); + restartCommunication("@text/offline.no-socket"); } } @@ -360,14 +381,15 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { byte[] buffer = new byte[4096]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); while (!Thread.interrupted()) { - String response = receive(socket, buffer, packet); + String response = receive(socket, packet); processMessage(socket, response); } } catch (IOException e) { - restartCommunication("Communication error in listener: " + e.getMessage()); + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); } } else { - restartCommunication("Error in communication, no socket to start listener"); + restartCommunication("@text/offline.no-socket"); } } @@ -382,7 +404,8 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { syncScenes(); getCurrentScene(); } catch (IOException e) { - restartCommunication("Error in communication refreshing device status: " + e.getMessage()); + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); } }, refreshInterval, refreshInterval, TimeUnit.SECONDS); } @@ -474,7 +497,7 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { } int deviceId = Integer.parseInt(answerContent.substring(0, 4), 16); - String deviceName = (new String(HexUtils.hexToBytes(answerContent.substring(4)))).replaceAll("[@$]*", ""); + String deviceName = ElroConnectsUtil.decode(answerContent.substring(4)); ElroConnectsDevice device = devices.get(deviceId); if (device != null) { device.setDeviceName(deviceName); @@ -491,7 +514,7 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { logger.debug("Could not decode answer {}", answerContent); return; } - sceneName = (new String(HexUtils.hexToBytes(answerContent.substring(6, 38)))).replaceAll("[@$]*", ""); + sceneName = ElroConnectsUtil.decode(answerContent.substring(6, 38)); scenes.put(sceneId, sceneName); logger.debug("Scene ID {} name: {}", sceneId, sceneName); } @@ -528,7 +551,21 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { if (handler != null) { handler.triggerAlarm(); } + // Also trigger an alarm on the bridge, so the alarm also comes through when no thing for the device is + // configured + triggerChannel(ALARM, Integer.toString(deviceId)); logger.debug("Device ID {} alarm", deviceId); + + if (answerContent.length() < 22) { + logger.debug("Could not get device status from alarm message for device {}", deviceId); + return; + } + String deviceStatus = answerContent.substring(14, 22); + ElroConnectsDevice device = devices.get(deviceId); + if (device != null) { + device.setDeviceStatus(deviceStatus); + device.updateState(); + } } private @Nullable ElroConnectsDevice addDevice(ElroConnectsMessage message) { @@ -609,7 +646,7 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { send(socket, query, broadcast); byte[] buffer = new byte[4096]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - return receive(socket, buffer, packet); + return receive(socket, packet); } private void send(DatagramSocket socket, String query, boolean broadcast) throws IOException { @@ -618,9 +655,9 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { : addr; if (address == null) { if (broadcast) { - restartCommunication("No broadcast address, check network configuration"); + restartCommunication("@text/offline.no-broadcast-address"); } else { - restartCommunication("Failed sending, hub address was not set"); + restartCommunication("@text/offline.no-hub-address"); } return; } @@ -630,9 +667,9 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { socket.send(queryPacket); } - private String receive(DatagramSocket socket, byte[] buffer, DatagramPacket packet) throws IOException { + private String receive(DatagramSocket socket, DatagramPacket packet) throws IOException { socket.receive(packet); - String response = new String(packet.getData(), packet.getOffset(), packet.getLength()); + String response = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8); logger.debug("Received: {}", response); addr = packet.getAddress(); return response; @@ -670,7 +707,66 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { String ctrlKey = this.ctrlKey; logger.debug("Device control {}, status {}", deviceId, deviceCommand); ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, - ELRO_DEVICE_CONTROL).withDeviceId(ElroConnectsUtil.encode(deviceId)).withDeviceStatus(deviceCommand); + ELRO_DEVICE_CONTROL, legacyFirmware).withDeviceId(deviceId).withDeviceStatus(deviceCommand); + sendElroMessage(elroMessage, false); + } + + public void renameDevice(int deviceId, String deviceName) throws IOException { + String connectorId = this.connectorId; + String ctrlKey = this.ctrlKey; + String encodedName = ElroConnectsUtil.encode(deviceName, 15); + encodedName = encodedName + ElroConnectsUtil.crc16(encodedName); + logger.debug("Rename device {} to {}", deviceId, deviceName); + ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, + ELRO_DEVICE_RENAME, legacyFirmware).withDeviceId(deviceId).withDeviceName(encodedName); + sendElroMessage(elroMessage, false); + } + + private void joinDevice() throws IOException { + String connectorId = this.connectorId; + String ctrlKey = this.ctrlKey; + logger.debug("Put hub in join device mode"); + ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, + ELRO_DEVICE_JOIN); + sendElroMessage(elroMessage, false); + } + + private void cancelJoinDevice() throws IOException { + String connectorId = this.connectorId; + String ctrlKey = this.ctrlKey; + logger.debug("Cancel hub in join device mode"); + ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, + ELRO_DEVICE_CANCEL_JOIN); + sendElroMessage(elroMessage, false); + } + + private void removeDevice(int deviceId) throws IOException { + if (devices.remove(deviceId) == null) { + logger.debug("Device {} not known, cannot remove", deviceId); + return; + } + ThingHandler handler = getDeviceHandler(deviceId); + if (handler != null) { + handler.dispose(); + } + String connectorId = this.connectorId; + String ctrlKey = this.ctrlKey; + logger.debug("Remove device {} from hub", deviceId); + ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, + ELRO_DEVICE_REMOVE, legacyFirmware).withDeviceId(deviceId); + sendElroMessage(elroMessage, false); + } + + private void replaceDevice(int deviceId) throws IOException { + if (getDevice(deviceId) == null) { + logger.debug("Device {} not known, cannot replace", deviceId); + return; + } + String connectorId = this.connectorId; + String ctrlKey = this.ctrlKey; + logger.debug("Replace device {} in hub", deviceId); + ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, + ELRO_DEVICE_REPLACE, legacyFirmware).withDeviceId(deviceId); sendElroMessage(elroMessage, false); } @@ -740,7 +836,7 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { String ctrlKey = this.ctrlKey; logger.debug("Select scene {}", scene); ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, - ELRO_SELECT_SCENE).withSceneType(ElroConnectsUtil.encode(scene)); + ELRO_SELECT_SCENE, legacyFirmware).withSceneType(scene); sendElroMessage(elroMessage, false); } @@ -754,7 +850,7 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { String ctrlKey = this.ctrlKey; logger.debug("Sync scenes"); ElroConnectsMessage elroMessage = new ElroConnectsMessage(msgIdIncrement(), connectorId, ctrlKey, - ELRO_SYNC_SCENES).withSceneGroup(ElroConnectsUtil.encode(0)).withSceneContent(SYNC_COMMAND) + ELRO_SYNC_SCENES, legacyFirmware).withSceneGroup(0).withSceneContent(SYNC_COMMAND) .withAnswerContent(SYNC_COMMAND); sendElroMessage(elroMessage, true); } @@ -762,17 +858,21 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { @Override public void handleCommand(ChannelUID channelUID, Command command) { logger.debug("Channel {}, command {}, type {}", channelUID, command, command.getClass()); - if (SCENE.equals(channelUID.getId())) { - if (command instanceof RefreshType) { - updateState(SCENE, new StringType(String.valueOf(currentScene))); - } else if (command instanceof DecimalType) { - try { - selectScene(((DecimalType) command).intValue()); - } catch (IOException e) { - restartCommunication("Error in communication while setting scene: " + e.getMessage()); - return; + try { + if (SCENE.equals(channelUID.getId())) { + if (command instanceof RefreshType) { + updateState(SCENE, new StringType(String.valueOf(currentScene))); + } else if (command instanceof StringType) { + try { + selectScene(Integer.valueOf(((StringType) command).toString())); + } catch (NumberFormatException nfe) { + logger.debug("Cannot interpret scene command {}", command); + } } } + } catch (IOException e) { + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); } } @@ -843,6 +943,10 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { return deviceHandlers.get(deviceId); } + public String getConnectorId() { + return connectorId; + } + public @Nullable ElroConnectsDevice getDevice(int deviceId) { return devices.get(deviceId); } @@ -861,4 +965,81 @@ public class ElroConnectsBridgeHandler extends BaseBridgeHandler { public Collection> getServices() { return Collections.singleton(ElroConnectsDiscoveryService.class); } + + public Map listDevicesFromConsole() { + return devices.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().getDeviceName())); + } + + public void refreshFromConsole() { + try { + keepAlive(); + getDeviceStatuses(); + getDeviceNames(); + } catch (IOException e) { + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); + } + } + + public void joinDeviceFromConsole() { + try { + joinDevice(); + } catch (IOException e) { + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); + } + } + + public void cancelJoinDeviceFromConsole() { + try { + cancelJoinDevice(); + } catch (IOException e) { + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); + } + } + + public boolean renameDeviceFromConsole(int deviceId, String deviceName) { + if (getDevice(deviceId) == null) { + return false; + } + try { + renameDevice(deviceId, deviceName); + } catch (IOException e) { + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); + } + return true; + } + + public boolean removeDeviceFromConsole(int deviceId) { + if (getDevice(deviceId) == null) { + return false; + } + try { + removeDevice(deviceId); + } catch (IOException e) { + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); + } + return true; + } + + public boolean replaceDeviceFromConsole(int deviceId) { + if (getDevice(deviceId) == null) { + return false; + } + try { + replaceDevice(deviceId); + } catch (IOException e) { + String msg = String.format("@text/offline.communication-error [ \"%s\" ]", e.getMessage()); + restartCommunication(msg); + } + return true; + } + + public void setDiscoveryService(ElroConnectsDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } } diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsDeviceHandler.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsDeviceHandler.java index 79ace7284f9..e55f7ea24a6 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsDeviceHandler.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/handler/ElroConnectsDeviceHandler.java @@ -53,6 +53,7 @@ public class ElroConnectsDeviceHandler extends BaseThingHandler { if (bridgeHandler != null) { bridgeHandler.setDeviceHandler(deviceId, this); updateProperties(bridgeHandler); + updateDeviceName(bridgeHandler); refreshChannels(bridgeHandler); } } @@ -74,15 +75,15 @@ public class ElroConnectsDeviceHandler extends BaseThingHandler { protected @Nullable ElroConnectsBridgeHandler getBridgeHandler() { Bridge bridge = getBridge(); if (bridge == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "No bridge defined for device " + String.valueOf(deviceId)); + String msg = String.format("@text/offline.no-bridge [ \"%d\" ]", deviceId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); return null; } ElroConnectsBridgeHandler bridgeHandler = (ElroConnectsBridgeHandler) bridge.getHandler(); if (bridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "No bridge handler defined for device " + String.valueOf(deviceId)); + String msg = String.format("@text/offline.no-bridge-handler [ \"%d\" ]", deviceId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); return null; } @@ -113,6 +114,14 @@ public class ElroConnectsDeviceHandler extends BaseThingHandler { } } + protected void updateDeviceName(ElroConnectsBridgeHandler bridgeHandler) { + ElroConnectsDevice device = bridgeHandler.getDevice(deviceId); + String deviceName = thing.getLabel(); + if ((device != null) && (deviceName != null)) { + device.updateDeviceName(deviceName); + } + } + /** * Refresh all thing channels. * diff --git a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/util/ElroConnectsUtil.java b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/util/ElroConnectsUtil.java index c23fb748849..7fe252eef21 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/util/ElroConnectsUtil.java +++ b/bundles/org.openhab.binding.elroconnects/src/main/java/org/openhab/binding/elroconnects/internal/util/ElroConnectsUtil.java @@ -12,8 +12,12 @@ */ package org.openhab.binding.elroconnects.internal.util; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.util.HexUtils; /** * The {@link ElroConnectsUtil} contains a few utility methods for the ELRO Connects binding. @@ -23,10 +27,78 @@ import org.eclipse.jdt.annotation.Nullable; @NonNullByDefault public final class ElroConnectsUtil { + private static final int POLYNOMIAL = 0x0000a001; // polynomial for CRC calculation + public static int encode(int value) { return (((value ^ 0xFFFFFFFF) + 0x10000) ^ 0x123) ^ 0x1234; } + public static int decode(int value, int msgId) { + return (byte) (0xFFFF + ~((value ^ 0x1234) ^ msgId)); + } + + /** + * Encode input string into hex + * + * @param input + * @param length byte length for input string in UTF-8 encoding, further characters will be cut + * @return encoded hex string cut to length + */ + public static String encode(String input, int length) { + byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + String content = "@".repeat(length - bytes.length) + new String(bytes, StandardCharsets.UTF_8) + "$"; + bytes = content.getBytes(StandardCharsets.UTF_8); + return HexUtils.bytesToHex(bytes); + } + + /** + * Decode hex string using UTF-8 encoding and drop leading @ characters and trailing $ + * + * @param input hex string + * @return string contained in input + */ + public static String decode(String input) { + return (new String(HexUtils.hexToBytes(input), StandardCharsets.UTF_8)).replaceAll("[@$]*", ""); + } + + /** + * Compare first bytes of byte representation of input strings in UTF-8 encoding + * + * @param string1 + * @param string2 + * @param length number of bytes to compare + * @return true if equal + */ + public static boolean equals(String string1, String string2, int length) { + byte[] bytes1 = Arrays.copyOf(string1.getBytes(StandardCharsets.UTF_8), length); + byte[] bytes2 = Arrays.copyOf(string2.getBytes(StandardCharsets.UTF_8), length); + return Arrays.equals(bytes1, bytes2); + } + + /** + * Calculate CRC-16 for input string. The input string should be treated as ASCII characters. The calculation is + * based on the MODBUS CRC-16 calculation. + * + * @param input + * @return crc hex format + */ + public static String crc16(String input) { + byte[] bytes = input.getBytes(StandardCharsets.US_ASCII); + int crc = 0x0000ffff; + for (byte curByte : bytes) { + crc ^= curByte; + for (int i = 0; i < 8; i++) { + if ((crc & 0x00000001) == 1) { + crc >>= 1; + crc ^= POLYNOMIAL; + } else { + crc >>= 1; + } + } + } + return Integer.toHexString(crc); + } + public static String stringOrEmpty(@Nullable String data) { return (data == null ? "" : data); } diff --git a/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/i18n/elroconnects.properties b/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/i18n/elroconnects.properties index 838f05f5d0f..1f0629ddf5d 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/i18n/elroconnects.properties +++ b/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/i18n/elroconnects.properties @@ -1,10 +1,12 @@ # binding binding.elroconnects.name = ELRO Connects Binding -binding.elroconnects.description = This is the binding for the ELRO Connects smart home system. +binding.elroconnects.description = This is the binding for the ELRO Connects smart home system # thing types +thing-type.elroconnects.account.label = ELRO Connects Account +thing-type.elroconnects.account.description = This bridge represents an ELRO Connects Account, used for discovering K1 Connector hubs linked to the account thing-type.elroconnects.coalarm.label = CO Alarm thing-type.elroconnects.coalarm.description = ELRO Connects CO alarm thing-type.elroconnects.connector.label = ELRO Connects Connector @@ -26,31 +28,41 @@ thing-type.elroconnects.wateralarm.description = ELRO Connects water alarm # thing types config +thing-type.config.elroconnects.account.username.label = Username +thing-type.config.elroconnects.account.username.description = Username for the ELRO Connects cloud account +thing-type.config.elroconnects.account.password.label = Password +thing-type.config.elroconnects.account.password.description = Password for the ELRO Connects cloud account +thing-type.config.elroconnects.account.enableBackgroundDiscovery.label = Background Discovery +thing-type.config.elroconnects.account.enableBackgroundDiscovery.description = Enable background discovery of hubs, polling the ELRO Connects cloud account every 5 min. thing-type.config.elroconnects.coalarm.deviceId.label = Device ID -thing-type.config.elroconnects.coalarm.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.coalarm.deviceId.description = ID of the ELRO Connects Device thing-type.config.elroconnects.connector.connectorId.label = Connector ID -thing-type.config.elroconnects.connector.connectorId.description = ID of the ELRO Connects K1 Connector, should be ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC address of the connector. +thing-type.config.elroconnects.connector.connectorId.description = ID of the ELRO Connects K1 Connector, should be ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC address of the connector thing-type.config.elroconnects.connector.ipAddress.label = IP Address -thing-type.config.elroconnects.connector.ipAddress.description = IP address of the ELRO Connects K1 Connector, not required if connector and openHAB server in same subnet. +thing-type.config.elroconnects.connector.ipAddress.description = IP address of the ELRO Connects K1 Connector, not required if connector and openHAB server in same subnet thing-type.config.elroconnects.connector.refreshInterval.label = Refresh Interval -thing-type.config.elroconnects.connector.refreshInterval.description = Heartbeat device refresh interval for communication with ELRO Connects K1 Connector in seconds, default 60s. +thing-type.config.elroconnects.connector.refreshInterval.description = Heartbeat device refresh interval for communication with ELRO Connects K1 Connector in seconds, default 60s +thing-type.config.elroconnects.connector.legacyFirmware.label = Legacy Firmware +thing-type.config.elroconnects.connector.legacyFirmware.description = Flag for legacy firmware, should be set to true if ELRO Connects K1 Connector firmware has version lower or equal to 2.0.14 thing-type.config.elroconnects.entrysensor.deviceId.label = Device ID -thing-type.config.elroconnects.entrysensor.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.entrysensor.deviceId.description = ID of the ELRO Connects Device thing-type.config.elroconnects.heatalarm.deviceId.label = Device ID -thing-type.config.elroconnects.heatalarm.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.heatalarm.deviceId.description = ID of the ELRO Connects Device thing-type.config.elroconnects.motionsensor.deviceId.label = Device ID -thing-type.config.elroconnects.motionsensor.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.motionsensor.deviceId.description = ID of the ELRO Connects Device thing-type.config.elroconnects.powersocket.deviceId.label = Device ID -thing-type.config.elroconnects.powersocket.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.powersocket.deviceId.description = ID of the ELRO Connects Device thing-type.config.elroconnects.smokealarm.deviceId.label = Device ID -thing-type.config.elroconnects.smokealarm.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.smokealarm.deviceId.description = ID of the ELRO Connects Device thing-type.config.elroconnects.temperaturesensor.deviceId.label = Device ID -thing-type.config.elroconnects.temperaturesensor.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.temperaturesensor.deviceId.description = ID of the ELRO Connects Device thing-type.config.elroconnects.wateralarm.deviceId.label = Device ID -thing-type.config.elroconnects.wateralarm.deviceId.description = ID of the ELRO Connects Device. +thing-type.config.elroconnects.wateralarm.deviceId.description = ID of the ELRO Connects Device # channel types +channel-type.elroconnects.alarm.label = Alarm +channel-type.elroconnects.alarm.description = Alarm triggered channel-type.elroconnects.coalarm.label = CO Alarm channel-type.elroconnects.coalarm.description = CO alarm triggered channel-type.elroconnects.entry.label = Entry Contact @@ -73,3 +85,22 @@ channel-type.elroconnects.testalarm.label = Test Alarm channel-type.elroconnects.testalarm.description = Trigger alarm test sound channel-type.elroconnects.wateralarm.label = Water Alarm channel-type.elroconnects.wateralarm.description = Water alarm triggered + +# thing status messages +offline.no-username = No username provided +offline.no-password = No password provided +offline.credentials-error = Invalid username or password +offline.request-timeout = Request timeout +offline.request-failed = Request failed + +offline.no-device-id = Device ID not set +offline.invalid-device-id = Device ID {0} not of format ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC address of the connector +offline.find-ip-fail = Error trying to find IP address for connector with ID {0} +offline.communication-data-error = Communication data error while starting communication +offline.communication-error = Error in communication: {0} +offline.no-socket = Error in communication, no socket to start listener +offline.no-broadcast-address = No broadcast address, check network configuration +offline.no-hub-address = Error in communication, hub address was not set + +offline.no-bridge = No bridge defined for device: {0} +offline.no-bridge-handler = No bridge handler defined for device: {0} diff --git a/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/thing/thing-types.xml index 6a6af9a8082..4d086204762 100644 --- a/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.elroconnects/src/main/resources/OH-INF/thing/thing-types.xml @@ -4,33 +4,63 @@ 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"> + + + This bridge represents an ELRO Connects Account, used for discovering K1 Connector hubs linked to the + account + + + + Username for the ELRO Connects cloud + + + + Password for the ELRO Connects cloud account + password + + + + Enable background discovery of hubs, polling the ELRO Connects cloud account every 5 min. + true + true + + + This bridge represents an ELRO Connects K1 Connector + connectorId ID of the ELRO Connects K1 Connector, should be ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC - address of the connector. + address of the connector IP address of the ELRO Connects K1 Connector, not required if connector and openHAB server in same - subnet. + subnet network-address true Heartbeat device refresh interval for communication with ELRO Connects K1 Connector in seconds, default - 60s. + 60s 60 true + + + Flag for legacy firmware, should be set to true if ELRO Connects K1 Connector firmware has version + lower or equal to 2.0.14 + false + true + @@ -45,6 +75,7 @@ + @@ -52,7 +83,7 @@ - ID of the ELRO Connects Device. + ID of the ELRO Connects Device @@ -66,6 +97,7 @@ + @@ -87,6 +119,7 @@ + @@ -94,7 +127,7 @@ - ID of the ELRO Connects Device. + ID of the ELRO Connects Device @@ -108,6 +141,7 @@ + @@ -115,7 +149,7 @@ - ID of the ELRO Connects Device. + ID of the ELRO Connects Device @@ -129,6 +163,7 @@ + @@ -136,7 +171,7 @@ - ID of the ELRO Connects Device. + ID of the ELRO Connects Device @@ -150,6 +185,7 @@ + @@ -157,7 +193,7 @@ - ID of the ELRO Connects Device. + ID of the ELRO Connects Device @@ -171,6 +207,7 @@ + @@ -178,7 +215,7 @@ - ID of the ELRO Connects Device. + ID of the ELRO Connects Device @@ -190,13 +227,14 @@ ELRO Connects power socket PowerOutlet + deviceId - ID of the ELRO Connects Device. + ID of the ELRO Connects Device @@ -207,6 +245,15 @@ Scene selection from scenes configured in the ELRO Connects app, enables configuring alarm modes + + trigger + + Alarm triggered + Alarm + + Alarm + + trigger