[elroconnects] Hub discovery, device management console commands, signal strength channels (#12653)

* Added bridge alarm channel
* Added signal strength channels
* Add deviceId payload to hub trigger channel
* Device renaming
* Device join functionality
* Console commands for hub management
* Remove device join channel
* Manual configuration README update
* Discovery of controllers using cloud service
* README update
* Support older firmware commands
* Fix error status messages
* Update ip address in thing configuration on ip detection
* Background discovery config parameter
* Fix enable discovery parameter, handle bad authentication response
* httpClient change, backgroundDiscovery enabled from thing handler

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege 2022-05-03 21:19:06 +02:00 committed by GitHub
parent 61de1a5387
commit cc3ebeffdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1586 additions and 175 deletions

View File

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

View File

@ -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<ThingTypeUID> SUPPORTED_BRIDGE_TYPES_UIDS = Set.of(THING_TYPE_CONNECTOR);
public static final Set<ThingTypeUID> 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<ThingTypeUID> SUPPORTED_ACCOUNT_TYPE_UIDS = Set.of(THING_TYPE_ACCOUNT);
public static final Set<ThingTypeUID> SUPPORTED_CONNECTOR_TYPES_UIDS = Set.of(THING_TYPE_CONNECTOR);
public static final Set<ThingTypeUID> 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<ThingTypeUID> SUPPORTED_BRIDGE_TYPES_UIDS = Stream
.concat(SUPPORTED_ACCOUNT_TYPE_UIDS.stream(), SUPPORTED_CONNECTOR_TYPES_UIDS.stream())
.collect(Collectors.toSet());
public static final Set<ThingTypeUID> 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<Integer, Integer> 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<Integer, Integer> 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<ElroDeviceType, String> 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<String> T_ENTRY_SENSOR = Set.of("0101", "1101", "2101");
public static final Set<String> T_POWERSOCKET = Set.of("0200", "1200", "2200");
public static final Set<String> T_MOTION_SENSOR = Set.of("0100", "1100", "2100");

View File

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

View File

@ -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() {

View File

@ -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<ElroConnectsBridgeHandler> 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<ElroConnectsBridgeHandler> 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<String> getUsages() {
return Arrays.asList(new String[] { buildCommandUsage(CONNECTORS, "list all K1 hub connector ID's"),
buildCommandUsage("<connectorId> " + REFRESH, "refresh device list, names and status"),
buildCommandUsage("<connectorId> " + DEVICES, "list all devices connected to the K1 hub"),
buildCommandUsage("<connectorId> " + RENAME + " <deviceId> <name>", "rename device with ID"),
buildCommandUsage("<connectorId> " + JOIN,
"put K1 hub in device join mode, 3 short presses on the device will join it to the hub"),
buildCommandUsage("<connectorId> " + CANCEL + " " + JOIN, "cancel K1 hub device join mode"),
buildCommandUsage("<connectorId> " + REPLACE + " <deviceId>",
"replace device with ID by newly joined device, puts K1 hub in join mode"),
buildCommandUsage("<connectorId> " + CANCEL + " " + REPLACE, "cancel K1 hub device replace mode"),
buildCommandUsage("<connectorId> " + REMOVE + " <deviceId>", "remove device with ID from K1 hub") });
}
}

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String, ElroConnectsConnector> 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<String, Object> 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;
}
}

View File

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

View File

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

View File

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

View File

@ -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<Map<String, String>>() {
}.getType();
private Type deviceListType = new TypeToken<List<ElroConnectsConnector>>() {
}.getType();
private final Map<String, String> 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<String, ElroConnectsConnector> 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<ElroConnectsConnector> 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<String, ElroConnectsConnector> 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<String, String> 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<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ElroConnectsBridgeDiscoveryService.class);
}
/**
* @return connectors on the account from the ELRO Connects cloud API
*/
public @Nullable Map<String, ElroConnectsConnector> 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;
}
}

View File

@ -25,4 +25,5 @@ public class ElroConnectsBridgeConfiguration {
public String connectorId = "";
public String ipAddress = "";
public int refreshInterval = 60;
public boolean legacyFirmware = false;
}

View File

@ -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<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ElroConnectsDiscoveryService.class);
}
public Map<Integer, String> 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;
}
}

View File

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

View File

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

View File

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

View File

@ -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">
<bridge-type id="account">
<label>ELRO Connects Account</label>
<description>This bridge represents an ELRO Connects Account, used for discovering K1 Connector hubs linked to the
account</description>
<config-description>
<parameter name="username" type="text" required="true">
<label>Username</label>
<description>Username for the ELRO Connects cloud</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<description>Password for the ELRO Connects cloud account</description>
<context>password</context>
</parameter>
<parameter name="enableBackgroundDiscovery" type="boolean">
<label>Background Discovery</label>
<description>Enable background discovery of hubs, polling the ELRO Connects cloud account every 5 min.</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<bridge-type id="connector">
<label>ELRO Connects Connector</label>
<description>This bridge represents an ELRO Connects K1 Connector</description>
<channels>
<channel id="scene" typeId="scene"/>
<channel id="alarm" typeId="alarm"/>
</channels>
<representation-property>connectorId</representation-property>
<config-description>
<parameter name="connectorId" type="text" required="true">
<label>Connector ID</label>
<description>ID of the ELRO Connects K1 Connector, should be ST_xxxxxxxxxxxx with xxxxxxxxxxxx the lowercase MAC
address of the connector.</description>
address of the connector</description>
</parameter>
<parameter name="ipAddress" type="text">
<label>IP Address</label>
<description>IP address of the ELRO Connects K1 Connector, not required if connector and openHAB server in same
subnet.</description>
subnet</description>
<context>network-address</context>
<advanced>true</advanced>
</parameter>
<parameter name="refreshInterval" type="integer">
<label>Refresh Interval</label>
<description>Heartbeat device refresh interval for communication with ELRO Connects K1 Connector in seconds, default
60s.</description>
60s</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="legacyFirmware" type="boolean">
<label>Legacy Firmware</label>
<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</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
@ -45,6 +75,7 @@
<channel id="smokeAlarm" typeId="smokealarm"/>
<channel id="muteAlarm" typeId="mutealarm"/>
<channel id="testAlarm" typeId="testalarm"/>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
@ -52,7 +83,7 @@
<config-description>
<parameter name="deviceId" type="integer" required="true">
<label>Device ID</label>
<description>ID of the ELRO Connects Device.</description>
<description>ID of the ELRO Connects Device</description>
</parameter>
</config-description>
</thing-type>
@ -66,6 +97,7 @@
<channel id="coAlarm" typeId="coalarm"/>
<channel id="muteAlarm" typeId="mutealarm"/>
<channel id="testAlarm" typeId="testalarm"/>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
@ -87,6 +119,7 @@
<channel id="heatAlarm" typeId="heatalarm"/>
<channel id="muteAlarm" typeId="mutealarm"/>
<channel id="testAlarm" typeId="testalarm"/>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
@ -94,7 +127,7 @@
<config-description>
<parameter name="deviceId" type="integer" required="true">
<label>Device ID</label>
<description>ID of the ELRO Connects Device.</description>
<description>ID of the ELRO Connects Device</description>
</parameter>
</config-description>
</thing-type>
@ -108,6 +141,7 @@
<channel id="waterAlarm" typeId="wateralarm"/>
<channel id="muteAlarm" typeId="mutealarm"/>
<channel id="testAlarm" typeId="testalarm"/>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
@ -115,7 +149,7 @@
<config-description>
<parameter name="deviceId" type="integer" required="true">
<label>Device ID</label>
<description>ID of the ELRO Connects Device.</description>
<description>ID of the ELRO Connects Device</description>
</parameter>
</config-description>
</thing-type>
@ -129,6 +163,7 @@
<channels>
<channel id="entryAlarm" typeId="entryalarm"/>
<channel id="entry" typeId="entry"/>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
@ -136,7 +171,7 @@
<config-description>
<parameter name="deviceId" type="integer" required="true">
<label>Device ID</label>
<description>ID of the ELRO Connects Device.</description>
<description>ID of the ELRO Connects Device</description>
</parameter>
</config-description>
</thing-type>
@ -150,6 +185,7 @@
<channels>
<channel id="motionAlarm" typeId="motionalarm"/>
<channel id="motion" typeId="system.motion"/>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
@ -157,7 +193,7 @@
<config-description>
<parameter name="deviceId" type="integer" required="true">
<label>Device ID</label>
<description>ID of the ELRO Connects Device.</description>
<description>ID of the ELRO Connects Device</description>
</parameter>
</config-description>
</thing-type>
@ -171,6 +207,7 @@
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="humidity" typeId="system.atmospheric-humidity"/>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
@ -178,7 +215,7 @@
<config-description>
<parameter name="deviceId" type="integer" required="true">
<label>Device ID</label>
<description>ID of the ELRO Connects Device.</description>
<description>ID of the ELRO Connects Device</description>
</parameter>
</config-description>
</thing-type>
@ -190,13 +227,14 @@
<description>ELRO Connects power socket</description>
<category>PowerOutlet</category>
<channels>
<channel id="signal" typeId="system.signal-strength"/>
<channel id="powerState" typeId="system.power"/>
</channels>
<representation-property>deviceId</representation-property>
<config-description>
<parameter name="deviceId" type="integer" required="true">
<label>Device ID</label>
<description>ID of the ELRO Connects Device.</description>
<description>ID of the ELRO Connects Device</description>
</parameter>
</config-description>
</thing-type>
@ -207,6 +245,15 @@
<description>Scene selection from scenes configured in the ELRO Connects app, enables configuring alarm modes</description>
</channel-type>
<channel-type id="alarm">
<kind>trigger</kind>
<label>Alarm</label>
<description>Alarm triggered</description>
<category>Alarm</category>
<tags>
<tag>Alarm</tag>
</tags>
</channel-type>
<channel-type id="smokealarm">
<kind>trigger</kind>
<label>Smoke Alarm</label>