mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[bluetooth.hdpowerview] New binding using Bluetooth Low Energy (#17099)
* [bluetooth.hdpowerview] initial contribution Signed-off-by: AndrewFG <software@whitebear.ch>
This commit is contained in:
parent
2fb5740b58
commit
313212709e
@ -50,6 +50,7 @@
|
||||
/bundles/org.openhab.binding.bluetooth.generic/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.grundfosalpha/ @tisoft
|
||||
/bundles/org.openhab.binding.bluetooth.hdpowerview/ @andrewfg
|
||||
/bundles/org.openhab.binding.bluetooth.radoneye/ @petero-dk
|
||||
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
|
||||
|
13
bundles/org.openhab.binding.bluetooth.hdpowerview/NOTICE
Normal file
13
bundles/org.openhab.binding.bluetooth.hdpowerview/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
81
bundles/org.openhab.binding.bluetooth.hdpowerview/README.md
Normal file
81
bundles/org.openhab.binding.bluetooth.hdpowerview/README.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Hunter Douglas (Luxaflex) PowerView Binding for Bluetooth
|
||||
|
||||
This is an openHAB binding for Bluetooth for [Hunter Douglas PowerView](https://www.hunterdouglas.com/operating-systems/motorized/powerview-motorization/overview) motorized shades via Bluetooth Low Energy (BLE).
|
||||
In some countries the PowerView system is sold under the brand name [Luxaflex](https://www.luxaflex.com/).
|
||||
|
||||
This binding supports Generation 3 shades connected directly via their in built Bluetooth Low Energy interface.
|
||||
There is a different binding [here](https://www.openhab.org/addons/bindings/hdpowerview/) for shades that are connected via a hub or gateway.
|
||||
|
||||
PowerView shades have motorization control for their vertical position, and some also have vane controls to change the angle of their slats.
|
||||
|
||||
## Supported Things
|
||||
|
||||
| Thing | Description |
|
||||
|-------|------------------------------------------------------------------------------------|
|
||||
| shade | A Powerview Generation 3 motorized shade connected via Bluetooth Low Energy (BLE). |
|
||||
|
||||
## Bluetooth Bridge
|
||||
|
||||
Before you can create `shade` Things, you must first create a Bluetooth Bridge to contain them.
|
||||
The instructions for creating a Bluetooth Bridge are [here](https://www.openhab.org/addons/bindings/bluetooth/).
|
||||
|
||||
## Discovery
|
||||
|
||||
Make sure your shades are visible via BLE in the PowerView app before attempting discovery.
|
||||
|
||||
The discovery process can be started by pressing the `+` button at the lower right of the Main UI Things page, selecting the Bluetooth binding, and pressing `Scan`.
|
||||
Any discovered shades will be displayed with the name prefix 'Powerview Shade'.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Configuration Parameter | Type | Description |
|
||||
|-------------------------|--------------------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| address | Required | The Bluetooth MAC address of the shade. |
|
||||
| bleTimeout | Optional, Advanced | The maximum number of seconds to wait before transactions over Bluetooth will time out (default = 6 seconds). |
|
||||
| heartbeatDelay | Optional, Advanced | The scanning interval in seconds at which the binding checks if the Shade is on- or off- line (default 15 seconds). |
|
||||
| pollingDelay | Optional, Advanced | The scanning interval in seconds at which the binding checks the battery status (default 300 seconds). |
|
||||
| encryptionKey | Optional | The key to be used when encrypting commands to the shade. See [next chapter](#encryption-key). |
|
||||
|
||||
## Encryption Key
|
||||
|
||||
If you want to send position commands to a shade, then an encryption key may be required.
|
||||
If the shade is NOT included in the Powerview App, then an encryption key is not required.
|
||||
But if it IS in the Powerview App, then openHAB has to use the same encryption key as used by the App.
|
||||
Currently you can only discover the encryption key by snooping the network traffic between the App and the shade.
|
||||
Please post on the openHAB community forum for advice about how to do this.
|
||||
|
||||
## Channels
|
||||
|
||||
A shade always implements a roller shutter channel `position` which controls the vertical position of the shade's (primary) rail.
|
||||
If the shade has slats or rotatable vanes, there is also a dimmer channel `tilt` which controls the slat / vane position.
|
||||
If it is a dual action (top-down plus bottom-up) shade, there is also a roller shutter channel `secondary` which controls the vertical position of the secondary rail.
|
||||
|
||||
| Channel | Item Type | Description |
|
||||
|---------------|----------------------|-------------------------------------------------------|
|
||||
| position | Rollershutter | The vertical position of the shade's rail. |
|
||||
| secondary | Rollershutter | The vertical position of the secondary rail (if any). |
|
||||
| tilt | Dimmer | The degree of opening of the slats or vanes (if any). |
|
||||
| battery-level | Number:Dimensionless | Battery level (10% = low, 50% = medium, 100% = high). |
|
||||
| rssi | Number:Power | Received Signal Strength Indication. |
|
||||
|
||||
Note: the channels `secondary` and `tilt` only exist if the shade physically supports such channels.
|
||||
|
||||
## Examples
|
||||
|
||||
```java
|
||||
// things
|
||||
Bridge bluetooth:bluegiga:abc123 "Bluetooth Stick" @ "Comms Cabinet" [port="COM3"] {
|
||||
// shade NOT integrated in Powerview App
|
||||
Thing bluetooth:shade:112233445566 "North Window Shade" @ "Office" [address="11:22:33:44:55:66"]
|
||||
|
||||
// or, shade integrated in Powerview App
|
||||
Thing bluetooth:shade:112233445566 "North Window Shade" @ "Office" [address="11:22:33:44:55:66", encryptionKey="59409c980e627e2fc702c2efcbd4064d"]
|
||||
}
|
||||
|
||||
// items
|
||||
Rollershutter Shade_Position "Shade Position" {channel="bluetooth:shade:abc123:112233445566:position"}
|
||||
Dimmer Shade_Position2 "Shade Position" {channel="bluetooth:shade:abc123:112233445566:position"}
|
||||
Dimmer Shade_Tilt "Shade Tilt" {channel="bluetooth:shade:abc123:112233445566:tilt"}
|
||||
Number:Dimensionless Shade_Battery_Level "Shade Battery Level" {channel="bluetooth:shade:abc123:112233445566:battery-level"}
|
||||
Number:Power Shade_RSSI "Shade Signal Strength" {channel="bluetooth:shade:abc123:112233445566:rssi"}
|
||||
```
|
71
bundles/org.openhab.binding.bluetooth.hdpowerview/pom.xml
Normal file
71
bundles/org.openhab.binding.bluetooth.hdpowerview/pom.xml
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.bluetooth.hdpowerview</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: HD Powerview Bluetooth Adapter</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/import</outputDirectory>
|
||||
<overwrite>true</overwrite>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>../org.openhab.binding.hdpowerview/src/main/java</directory>
|
||||
<includes>
|
||||
<include>**/ShadeCapabilitiesDatabase.java</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>add-source</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>${project.build.directory}/import</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.bluetooth.hdpowerview-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-bluetooth-hdpowerview" description="HD Powerview Bluetooth Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-serial</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.hdpowerview/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
</features>
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.hdpowerview.internal;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link ShadeBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeBindingConstants {
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_SHADE = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "shade");
|
||||
|
||||
public static final String CHANNEL_SHADE_PRIMARY = "primary";
|
||||
public static final String CHANNEL_SHADE_SECONDARY = "secondary";
|
||||
public static final String CHANNEL_SHADE_TILT = "tilt";
|
||||
public static final String CHANNEL_SHADE_BATTERY_LEVEL = "battery-level";
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SHADE);
|
||||
|
||||
public static final int HUNTER_DOUGLAS_MANUFACTURER_ID = 0x819;
|
||||
|
||||
public static final Map<UUID, String> MAP_UID_PROPERTY_NAMES = Map.of( //
|
||||
GattCharacteristic.MANUFACTURER_NAME_STRING.getUUID(), Thing.PROPERTY_VENDOR, //
|
||||
GattCharacteristic.HARDWARE_REVISION_STRING.getUUID(), Thing.PROPERTY_HARDWARE_VERSION, //
|
||||
GattCharacteristic.FIRMWARE_REVISION_STRING.getUUID(), Thing.PROPERTY_FIRMWARE_VERSION, //
|
||||
GattCharacteristic.SERIAL_NUMBER_STRING.getUUID(), Thing.PROPERTY_SERIAL_NUMBER, //
|
||||
GattCharacteristic.MODEL_NUMBER_STRING.getUUID(), Thing.PROPERTY_MODEL_ID);
|
||||
|
||||
public static final String HUNTER_DOUGLAS = "Hunter Douglas";
|
||||
public static final String SHADE_LABEL = "PowerView Shade";
|
||||
|
||||
public static final String PROPERTY_HOME_ID = "homeId";
|
||||
public static final String PROPERTY_ENCRYPTION_KEY = "encryptionKey";
|
||||
|
||||
public static final UUID UUID_SERVICE_SHADE = UUID.fromString("0000FDC1-0000-1000-8000-00805F9B34FB");
|
||||
public static final UUID UUID_CHARACTERISTIC_POSITION = UUID.fromString("CAFE1001-C0FF-EE01-8000-A110CA7AB1E0");
|
||||
public static final UUID UUID_CHARACTERISTIC_TBD = UUID.fromString("CAFE1002-C0FF-EE01-8000-A110CA7AB1E0");
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.hdpowerview.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.bluetooth.BluetoothBindingConstants.*;
|
||||
import static org.openhab.binding.bluetooth.hdpowerview.internal.ShadeBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* Discovery participant recognizes Hunter Douglas Powerview Shades and create discovery results for them.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component
|
||||
public class ShadeDiscoveryParticipant implements BluetoothDiscoveryParticipant {
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return SUPPORTED_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
|
||||
Integer manufacturerId = device.getManufacturerId();
|
||||
if (manufacturerId != null && manufacturerId.intValue() == HUNTER_DOUGLAS_MANUFACTURER_ID) {
|
||||
return new ThingUID(THING_TYPE_SHADE, device.getAdapter().getUID(),
|
||||
device.getAddress().toString().toLowerCase().replace(":", ""));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
|
||||
ThingUID thingUID = getThingUID(device);
|
||||
if (thingUID != null) {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
properties.put(CONFIGURATION_ADDRESS, device.getAddress().toString());
|
||||
properties.put(Thing.PROPERTY_VENDOR, HUNTER_DOUGLAS);
|
||||
properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getAddress().toString());
|
||||
|
||||
String serialNumber = device.getSerialNumber();
|
||||
if (serialNumber != null) {
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
|
||||
}
|
||||
|
||||
String firmwareRevision = device.getFirmwareRevision();
|
||||
if (firmwareRevision != null) {
|
||||
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareRevision);
|
||||
}
|
||||
|
||||
String model = device.getModel();
|
||||
if (model != null) {
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model);
|
||||
}
|
||||
|
||||
String hardwareRevision = device.getHardwareRevision();
|
||||
if (hardwareRevision != null) {
|
||||
properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwareRevision);
|
||||
}
|
||||
|
||||
Integer txPower = device.getTxPower();
|
||||
if (txPower != null) {
|
||||
properties.put(PROPERTY_TXPOWER, Integer.toString(txPower));
|
||||
}
|
||||
|
||||
String label = String.format("%s (%s)", SHADE_LABEL, device.getName());
|
||||
|
||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withRepresentationProperty(CONFIGURATION_ADDRESS).withBridge(device.getAdapter().getUID())
|
||||
.withLabel(label).build();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresConnection(BluetoothDiscoveryDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
// we want to go first
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.hdpowerview.internal.factory;
|
||||
|
||||
import static org.openhab.binding.bluetooth.hdpowerview.internal.ShadeBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.hdpowerview.internal.shade.ShadeHandler;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link ShadeHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.bluetooth.hdpowerview", service = ThingHandlerFactory.class)
|
||||
public class ShadeHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
if (THING_TYPE_SHADE.equals(thingTypeUID)) {
|
||||
return new ShadeHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.hdpowerview.internal.shade;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link ShadeConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeConfiguration {
|
||||
public String address = "";
|
||||
public int bleTimeout = 6; // seconds
|
||||
public int heartbeatDelay = 15; // seconds
|
||||
public int pollingDelay = 300; // seconds
|
||||
public String encryptionKey = "";
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("[address:%s, bleTimeout:%d, heartbeatDelay:%d, pollingDelay:%d]", address, bleTimeout,
|
||||
heartbeatDelay, pollingDelay);
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.hdpowerview.internal.shade;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
|
||||
/**
|
||||
* Parser for data returned by an HD PowerView Generation 3 Shade.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeDataReader {
|
||||
|
||||
// internal values 0 to 4000 scale to real position values 0% to 100%
|
||||
private static final double SCALE = 40;
|
||||
|
||||
// indexes to data field positions in the incoming bytes
|
||||
private static final int INDEX_MANUFACTURER_ID = 0;
|
||||
private static final int INDEX_HOME_ID = 2;
|
||||
private static final int INDEX_TYPE_ID = 4;
|
||||
private static final int INDEX_PRIMARY = 5;
|
||||
private static final int INDEX_SECONDARY = 7;
|
||||
private static final int INDEX_TILT = 9;
|
||||
private static final int INDEX_VELOCITY = 10;
|
||||
|
||||
private int manufacturerId;
|
||||
private int homeId;
|
||||
private int typeId;
|
||||
private double primary;
|
||||
private double secondary;
|
||||
private double tilt;
|
||||
private double velocity; // not 100% sure about this
|
||||
|
||||
public ShadeDataReader() {
|
||||
}
|
||||
|
||||
public int getManufacturerId() {
|
||||
return manufacturerId;
|
||||
}
|
||||
|
||||
public int getHomeId() {
|
||||
return homeId;
|
||||
}
|
||||
|
||||
public PercentType getPrimary() {
|
||||
return new PercentType(BigDecimal.valueOf(primary));
|
||||
}
|
||||
|
||||
public PercentType getSecondary() {
|
||||
return new PercentType(BigDecimal.valueOf(secondary));
|
||||
}
|
||||
|
||||
public PercentType getTilt() {
|
||||
return new PercentType(BigDecimal.valueOf(tilt));
|
||||
}
|
||||
|
||||
public int getTypeId() {
|
||||
return typeId;
|
||||
}
|
||||
|
||||
public double getVelocity() {
|
||||
return velocity;
|
||||
}
|
||||
|
||||
public ShadeDataReader setBytes(byte[] bytes) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
manufacturerId = buffer.getShort(INDEX_MANUFACTURER_ID);
|
||||
homeId = buffer.getShort(INDEX_HOME_ID);
|
||||
typeId = buffer.get(INDEX_TYPE_ID);
|
||||
velocity = buffer.get(INDEX_VELOCITY);
|
||||
|
||||
primary = Math.max(0, Math.min(100, buffer.getShort(INDEX_PRIMARY) / SCALE));
|
||||
secondary = Math.max(0, Math.min(100, buffer.getShort(INDEX_SECONDARY) / SCALE));
|
||||
tilt = Math.max(0, Math.min(100, buffer.get(INDEX_TILT)));
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.hdpowerview.internal.shade;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Encoder/decoder for data sent to an HD PowerView Generation 3 Shade.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeDataWriter {
|
||||
|
||||
// real position values 0% to 100% scale to internal values 0 to 10000
|
||||
private static final double SCALE = 100;
|
||||
|
||||
// byte array for a blank 'no-op' write command
|
||||
private static final byte[] BLANK_WRITE_COMMAND_FRAME = HexFormat.ofDelimiter(":")
|
||||
.parseHex("f7:01:00:09:00:80:00:80:00:80:00:80:00");
|
||||
|
||||
// index to data field positions in the outgoing bytes
|
||||
private static final int INDEX_SEQUENCE = 2;
|
||||
private static final int INDEX_PRIMARY = 4;
|
||||
private static final int INDEX_SECONDARY = 6;
|
||||
private static final int INDEX_TILT = 10;
|
||||
|
||||
private final byte[] bytes;
|
||||
|
||||
public ShadeDataWriter() {
|
||||
bytes = BLANK_WRITE_COMMAND_FRAME.clone();
|
||||
}
|
||||
|
||||
public ShadeDataWriter(byte[] bytes) {
|
||||
this.bytes = bytes.clone();
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the bytes using the given hexadecimal key. No-Op if key is blank or null.
|
||||
*
|
||||
* @param keyHex decryption key
|
||||
* @return decrypted bytes
|
||||
* @throws IllegalArgumentException (the key hex value could not be parsed)
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws NoSuchPaddingException
|
||||
* @throws InvalidKeyException
|
||||
* @throws InvalidAlgorithmParameterException
|
||||
* @throws IllegalBlockSizeException
|
||||
* @throws BadPaddingException
|
||||
*/
|
||||
public byte[] getDecrypted(@Nullable String keyHex)
|
||||
throws IllegalArgumentException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
|
||||
InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
|
||||
if (keyHex != null && !keyHex.isBlank()) {
|
||||
byte[] keyBytes = HexFormat.of().parseHex(keyHex);
|
||||
SecretKey keySecret = new SecretKeySpec(keyBytes, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySecret, new IvParameterSpec(new byte[16]));
|
||||
return cipher.doFinal(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the bytes using the given hexadecimal key. No-Op if key is blank or null.
|
||||
*
|
||||
* @param keyHex decryption key
|
||||
* @return encrypted bytes
|
||||
* @throws IllegalArgumentException (the key hex value could not be parsed)
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws NoSuchPaddingException
|
||||
* @throws InvalidKeyException
|
||||
* @throws InvalidAlgorithmParameterException
|
||||
* @throws IllegalBlockSizeException
|
||||
* @throws BadPaddingException
|
||||
*/
|
||||
public byte[] getEncrypted(@Nullable String keyHex)
|
||||
throws IllegalArgumentException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
|
||||
InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
|
||||
if (keyHex != null && !keyHex.isBlank()) {
|
||||
byte[] keyBytes = HexFormat.of().parseHex(keyHex);
|
||||
SecretKey keySecret = new SecretKeySpec(keyBytes, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySecret, new IvParameterSpec(new byte[16]));
|
||||
return cipher.doFinal(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the bytes in little endian format.
|
||||
*/
|
||||
public byte[] encodeLE(double percent) throws IllegalArgumentException {
|
||||
if (percent < 0 || percent > 100) {
|
||||
throw new IllegalArgumentException(String.format("Number '%0.1f' out of range (0% to 100%)", percent));
|
||||
}
|
||||
int position = ((int) Math.round(percent * SCALE));
|
||||
return new byte[] { (byte) (position & 0xff), (byte) ((position & 0xff00) >> 8) };
|
||||
}
|
||||
|
||||
public ShadeDataWriter withPrimary(double percent) {
|
||||
byte[] bytes = encodeLE(percent);
|
||||
System.arraycopy(bytes, 0, this.bytes, INDEX_PRIMARY, bytes.length);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShadeDataWriter withSecondary(double percent) {
|
||||
byte[] bytes = encodeLE(percent);
|
||||
System.arraycopy(bytes, 0, this.bytes, INDEX_SECONDARY, bytes.length);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShadeDataWriter withSequence(byte sequence) {
|
||||
this.bytes[INDEX_SEQUENCE] = sequence;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShadeDataWriter withTilt(double percent) {
|
||||
if (percent < 0 || percent > 100) {
|
||||
throw new IllegalArgumentException(String.format("Number '%0.1f' out of range (0% to 100%)", percent));
|
||||
}
|
||||
byte[] bytes = new byte[] { (byte) (percent), 0 };
|
||||
System.arraycopy(bytes, 0, this.bytes, INDEX_TILT, bytes.length);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,540 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.hdpowerview.internal.shade;
|
||||
|
||||
import static org.openhab.binding.bluetooth.hdpowerview.internal.ShadeBindingConstants.*;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.BluetoothAddress;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.BluetoothService;
|
||||
import org.openhab.binding.bluetooth.BluetoothUtils;
|
||||
import org.openhab.binding.bluetooth.ConnectionException;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||
import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
|
||||
import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ShadeHandler} is a thing handler for Hunter Douglas Powerview Shades using Bluetooth Low Energy (BLE).
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeHandler extends BeaconBluetoothHandler {
|
||||
|
||||
private static final String ENCRYPTION_KEY_HELP_URL = //
|
||||
"https://www.openhab.org/addons/bindings/bluetooth.hdpowerview/readme.html#encryption-key";
|
||||
|
||||
private static final ShadeCapabilitiesDatabase CAPABILITIES_DATABASE = new ShadeCapabilitiesDatabase();
|
||||
private static final Map<Integer, String> HOME_ID_ENCRYPTION_KEYS = new ConcurrentHashMap<>();
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ShadeHandler.class);
|
||||
private final List<Future<?>> readTasks = new ArrayList<>();
|
||||
private final Map<Instant, Future<?>> writeTasks = new ConcurrentHashMap<>();
|
||||
private final ShadeDataReader dataReader = new ShadeDataReader();
|
||||
|
||||
private @Nullable Capabilities capabilities;
|
||||
private @Nullable Future<?> readBatteryTask;
|
||||
|
||||
private byte[] cachedValue = new byte[0];
|
||||
private Instant activityTimeout = Instant.MIN;
|
||||
private ShadeConfiguration configuration = new ShadeConfiguration();
|
||||
private boolean propertiesLoaded = false;
|
||||
private byte writeSequence = Byte.MIN_VALUE;
|
||||
private int homeId;
|
||||
|
||||
public ShadeHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the given task
|
||||
*/
|
||||
private void cancelTask(@Nullable Future<?> task, boolean interrupt) {
|
||||
if (task != null) {
|
||||
task.cancel(interrupt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all tasks
|
||||
*/
|
||||
private void cancelTasks(boolean interrupt) {
|
||||
readTasks.forEach(task -> cancelTask(task, interrupt));
|
||||
writeTasks.values().forEach(task -> cancelTask(task, interrupt));
|
||||
cancelTask(readBatteryTask, interrupt);
|
||||
readBatteryTask = null;
|
||||
readTasks.clear();
|
||||
writeTasks.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
if (CHANNEL_SHADE_BATTERY_LEVEL.equals(channelUID.getId())) {
|
||||
scheduleReadBattery();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the device and download its services (if not already done). Blocks until the operation completes.
|
||||
*/
|
||||
private void connectAndWait() throws TimeoutException, InterruptedException, ConnectionException {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING) {
|
||||
if (!device.connect()) {
|
||||
throw new ConnectionException("Failed to start connecting");
|
||||
}
|
||||
}
|
||||
if (!device.awaitConnection(configuration.bleTimeout, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Connection attempt timeout");
|
||||
}
|
||||
}
|
||||
if (!device.isServicesDiscovered()) {
|
||||
device.discoverServices();
|
||||
if (!device.awaitServiceDiscovery(configuration.bleTimeout, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Service discovery timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
cancelTasks(true);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for encrypting write commands. Uses either..
|
||||
*
|
||||
* <li>The key for this specific Thing via its own configuration properties, or</li>
|
||||
* <li>The key for any other Thing with the same homeId via the shared ENCRYPTION_KEYS map</li>
|
||||
*/
|
||||
private @Nullable String getEncryptionKey() {
|
||||
String key = null;
|
||||
if (homeId != 0) {
|
||||
key = configuration.encryptionKey;
|
||||
key = key.isBlank() ? HOME_ID_ENCRYPTION_KEYS.get(homeId) : key;
|
||||
if (key == null || key.isBlank()) {
|
||||
logger.warn("Device '{}' requires an encryption key => see {}", device.getAddress(),
|
||||
ENCRYPTION_KEY_HELP_URL);
|
||||
} else {
|
||||
HOME_ID_ENCRYPTION_KEYS.putIfAbsent(homeId, key);
|
||||
if (!configuration.encryptionKey.equals(key)) {
|
||||
configuration.encryptionKey = key;
|
||||
Configuration config = getConfig();
|
||||
config.put(PROPERTY_ENCRYPTION_KEY, key);
|
||||
updateConfiguration(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command commandArg) {
|
||||
super.handleCommand(channelUID, commandArg);
|
||||
|
||||
if (commandArg == RefreshType.REFRESH) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_SHADE_BATTERY_LEVEL:
|
||||
scheduleReadBattery();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Command command = commandArg;
|
||||
|
||||
// convert stop commands to (current) position commands
|
||||
if (command instanceof StopMoveType stopMove) {
|
||||
if (StopMoveType.STOP == stopMove) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_SHADE_PRIMARY:
|
||||
command = dataReader.getPrimary();
|
||||
break;
|
||||
case CHANNEL_SHADE_SECONDARY:
|
||||
command = dataReader.getSecondary();
|
||||
break;
|
||||
case CHANNEL_SHADE_TILT:
|
||||
command = dataReader.getTilt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert up/down commands to position command
|
||||
if (command instanceof UpDownType updown) {
|
||||
command = UpDownType.DOWN == updown ? PercentType.ZERO : PercentType.HUNDRED;
|
||||
}
|
||||
|
||||
if (command instanceof PercentType percent) {
|
||||
Capabilities capabilities = this.capabilities;
|
||||
if (capabilities == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_SHADE_PRIMARY:
|
||||
if (capabilities.supportsPrimary()) {
|
||||
scheduleWritePosition(new ShadeDataWriter().withSequence(writeSequence++)
|
||||
.withPrimary(percent.doubleValue()).getEncrypted(getEncryptionKey()));
|
||||
}
|
||||
break;
|
||||
|
||||
case CHANNEL_SHADE_SECONDARY:
|
||||
if (capabilities.supportsSecondary()) {
|
||||
scheduleWritePosition(new ShadeDataWriter().withSequence(writeSequence++)
|
||||
.withSecondary(percent.doubleValue()).getEncrypted(getEncryptionKey()));
|
||||
}
|
||||
break;
|
||||
|
||||
case CHANNEL_SHADE_TILT:
|
||||
if (capabilities.supportsTiltOnClosed() || capabilities.supportsTilt180()
|
||||
|| capabilities.supportsTiltAnywhere()) {
|
||||
scheduleWritePosition(new ShadeDataWriter().withSequence(writeSequence++)
|
||||
.withTilt(percent.doubleValue()).getEncrypted(getEncryptionKey()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (InvalidKeyException | IllegalArgumentException | NoSuchAlgorithmException | NoSuchPaddingException
|
||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
logger.warn("handleCommand() device={} error={}", device.getAddress(), e.getMessage(),
|
||||
logger.isDebugEnabled() ? e : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
super.initialize();
|
||||
configuration = getConfigAs(ShadeConfiguration.class);
|
||||
try {
|
||||
new BluetoothAddress(configuration.address);
|
||||
} catch (IllegalArgumentException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
return;
|
||||
}
|
||||
updateProperty(PROPERTY_HOME_ID, Integer.toHexString(homeId).toUpperCase());
|
||||
activityTimeout = Instant.now().plusSeconds(configuration.pollingDelay * 2);
|
||||
|
||||
cancelTasks(false);
|
||||
|
||||
int initialDelaySeconds = 0;
|
||||
readTasks.add(scheduler.scheduleWithFixedDelay(() -> readThingStatus(), ++initialDelaySeconds,
|
||||
configuration.heartbeatDelay, TimeUnit.SECONDS));
|
||||
readTasks.add(scheduler.scheduleWithFixedDelay(() -> readProperties(), ++initialDelaySeconds,
|
||||
configuration.heartbeatDelay, TimeUnit.SECONDS));
|
||||
readTasks.add(scheduler.scheduleWithFixedDelay(() -> readBattery(), ++initialDelaySeconds,
|
||||
configuration.pollingDelay, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivity() {
|
||||
super.onActivity();
|
||||
if (thing.getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
activityTimeout = Instant.now().plusSeconds(configuration.pollingDelay * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the scan record and update the channels.
|
||||
*/
|
||||
@Override
|
||||
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
||||
super.onScanRecordReceived(scanNotification);
|
||||
onActivity();
|
||||
byte[] value = scanNotification.getManufacturerData();
|
||||
if (Arrays.equals(cachedValue, value)) {
|
||||
return;
|
||||
}
|
||||
cachedValue = value;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("onScanRecordReceived() device={} received value={}", device.getAddress(),
|
||||
HexUtils.bytesToHex(value, ":"));
|
||||
}
|
||||
updatePosition(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
super.onServicesDiscovered();
|
||||
scheduleReadBattery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the battery state. Blocks until the operation completes.
|
||||
*/
|
||||
private void readBattery() {
|
||||
synchronized (this) {
|
||||
if (device.isServicesDiscovered()) {
|
||||
try {
|
||||
connectAndWait();
|
||||
for (BluetoothService service : device.getServices()) {
|
||||
BluetoothCharacteristic characteristic = service
|
||||
.getCharacteristic(GattCharacteristic.BATTERY_LEVEL.getUUID());
|
||||
if (characteristic != null && characteristic.canRead()) {
|
||||
byte[] value = device.readCharacteristic(characteristic).get(configuration.bleTimeout,
|
||||
TimeUnit.SECONDS);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("readBattery() device={} read uuid={}, value={}", device.getAddress(),
|
||||
characteristic.getUuid(), HexUtils.bytesToHex(value, ":"));
|
||||
}
|
||||
updateState(CHANNEL_SHADE_BATTERY_LEVEL,
|
||||
value.length > 0 ? QuantityType.valueOf(value[0], Units.PERCENT) : UnDefType.UNDEF);
|
||||
onActivity();
|
||||
}
|
||||
}
|
||||
} catch (ConnectionException | TimeoutException | ExecutionException | InterruptedException e) {
|
||||
// Bluetooth has frequent errors so we do not normally log them
|
||||
logger.debug("readBattery() device={}, error={}", device.getAddress(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the thing properties. Blocks until the operation completes.
|
||||
*/
|
||||
private void readProperties() {
|
||||
synchronized (this) {
|
||||
if (!propertiesLoaded && device.isServicesDiscovered()) {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
try {
|
||||
connectAndWait();
|
||||
for (BluetoothService service : device.getServices()) {
|
||||
for (Entry<UUID, String> property : MAP_UID_PROPERTY_NAMES.entrySet()) {
|
||||
BluetoothCharacteristic characteristic = service.getCharacteristic(property.getKey());
|
||||
if (characteristic != null && characteristic.canRead()) {
|
||||
byte[] value = device.readCharacteristic(characteristic).get(configuration.bleTimeout,
|
||||
TimeUnit.SECONDS);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("readProperties() device={} read uuid={}, value={}",
|
||||
device.getAddress(), characteristic.getUuid(),
|
||||
HexUtils.bytesToHex(value, ":"));
|
||||
}
|
||||
String propertyName = property.getValue();
|
||||
String propertyValue = BluetoothUtils.getStringValue(value, 0);
|
||||
if (propertyValue != null) {
|
||||
properties.put(propertyName, propertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ConnectionException | TimeoutException | ExecutionException | InterruptedException e) {
|
||||
// Bluetooth has frequent errors so we do not normally log them
|
||||
logger.debug("readProperties() device={}, error={}", device.getAddress(), e.getMessage());
|
||||
} finally {
|
||||
if (!properties.isEmpty()) {
|
||||
propertiesLoaded = true;
|
||||
properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getAddress().toString());
|
||||
thing.setProperties(properties);
|
||||
onActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the Bluetooth services. Blocks until the operation completes.
|
||||
*/
|
||||
private void readServices() {
|
||||
synchronized (this) {
|
||||
if (!device.isServicesDiscovered()) {
|
||||
try {
|
||||
connectAndWait();
|
||||
onActivity();
|
||||
} catch (ConnectionException | TimeoutException | InterruptedException e) {
|
||||
// Bluetooth has frequent errors so we do not normally log them
|
||||
logger.debug("readServices() device={}, error={}", device.getAddress(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Heartbeat task. Updates the online state and ensures that services are loaded.
|
||||
*/
|
||||
private void readThingStatus() {
|
||||
if (thing.getStatus() == ThingStatus.ONLINE) {
|
||||
if (Instant.now().isAfter(activityTimeout)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
} else {
|
||||
readServices();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a readBattery command
|
||||
*/
|
||||
private void scheduleReadBattery() {
|
||||
cancelTask(readBatteryTask, false);
|
||||
readBatteryTask = scheduler.submit(() -> readBattery());
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a writePosition command with the given value
|
||||
*/
|
||||
private void scheduleWritePosition(byte[] value) {
|
||||
Instant taskId = Instant.now();
|
||||
writeTasks.put(taskId, scheduler.submit(() -> writePosition(taskId, value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update homeId and if necessary update the encryption key.
|
||||
*/
|
||||
private void updateHomeId(int newHomeId) {
|
||||
if (homeId != newHomeId) {
|
||||
homeId = newHomeId;
|
||||
updateProperty(PROPERTY_HOME_ID, Integer.toHexString(homeId).toUpperCase());
|
||||
getEncryptionKey();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position channels
|
||||
*/
|
||||
private void updatePosition(byte[] value) {
|
||||
logger.debug("updatePosition() device={}", device.getAddress());
|
||||
dataReader.setBytes(value);
|
||||
updateHomeId(dataReader.getHomeId());
|
||||
|
||||
Capabilities capabilities = this.capabilities;
|
||||
if (capabilities == null) {
|
||||
capabilities = CAPABILITIES_DATABASE.getCapabilities(dataReader.getTypeId(), null);
|
||||
this.capabilities = capabilities;
|
||||
|
||||
// remove unused channels
|
||||
List<Channel> removeChannels = new ArrayList<>();
|
||||
Channel channel;
|
||||
if (!capabilities.supportsPrimary()) {
|
||||
channel = thing.getChannel(CHANNEL_SHADE_PRIMARY);
|
||||
if (channel != null) {
|
||||
removeChannels.add(channel);
|
||||
}
|
||||
}
|
||||
if (!capabilities.supportsSecondary()) {
|
||||
channel = thing.getChannel(CHANNEL_SHADE_SECONDARY);
|
||||
if (channel != null) {
|
||||
removeChannels.add(channel);
|
||||
}
|
||||
}
|
||||
if (!(capabilities.supportsTilt180() || capabilities.supportsTiltAnywhere()
|
||||
|| capabilities.supportsTiltOnClosed())) {
|
||||
channel = thing.getChannel(CHANNEL_SHADE_TILT);
|
||||
if (channel != null) {
|
||||
removeChannels.add(channel);
|
||||
}
|
||||
}
|
||||
if (!removeChannels.isEmpty()) {
|
||||
updateThing(editThing().withoutChannels(removeChannels).build());
|
||||
}
|
||||
}
|
||||
|
||||
// update channel states
|
||||
if (capabilities.supportsPrimary()) {
|
||||
updateState(CHANNEL_SHADE_PRIMARY, dataReader.getPrimary());
|
||||
}
|
||||
if (capabilities.supportsSecondary()) {
|
||||
updateState(CHANNEL_SHADE_SECONDARY, dataReader.getSecondary());
|
||||
}
|
||||
if (capabilities.supportsTilt180() || capabilities.supportsTiltAnywhere()
|
||||
|| capabilities.supportsTiltOnClosed()) {
|
||||
updateState(CHANNEL_SHADE_TILT, dataReader.getTilt());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write position channel value task. Blocks until the operation completes.
|
||||
*
|
||||
* @param taskId identifies the task entry in the writeTasks map
|
||||
* @param value the data to write
|
||||
*/
|
||||
private void writePosition(Instant taskId, byte[] value) {
|
||||
synchronized (this) {
|
||||
try {
|
||||
if (device.isServicesDiscovered()) {
|
||||
connectAndWait();
|
||||
BluetoothService shadeService = device.getServices(UUID_SERVICE_SHADE);
|
||||
if (shadeService != null) {
|
||||
BluetoothCharacteristic characteristic = shadeService
|
||||
.getCharacteristic(UUID_CHARACTERISTIC_POSITION);
|
||||
if (characteristic != null) {
|
||||
device.writeCharacteristic(characteristic, value).get(configuration.bleTimeout,
|
||||
TimeUnit.SECONDS);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("writePosition() device={} sent uuid={}, value={}", device.getAddress(),
|
||||
characteristic.getUuid(), HexUtils.bytesToHex(value, ":"));
|
||||
}
|
||||
onActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ConnectionException | TimeoutException | ExecutionException | InterruptedException e) {
|
||||
// Bluetooth has frequent errors so we do not normally log them
|
||||
logger.debug("writePosition() device={}, error={}", device.getAddress(), e.getMessage());
|
||||
} finally {
|
||||
writeTasks.remove(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
# thing types
|
||||
|
||||
thing-type.bluetooth.shade.label = PowerView Shade
|
||||
thing-type.bluetooth.shade.description = Hunter Douglas (Luxaflex) PowerView Gen3 Shade
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.bluetooth.shade.address.label = Address
|
||||
thing-type.config.bluetooth.shade.address.description = Bluetooth address in XX:XX:XX:XX:XX:XX format
|
||||
thing-type.config.bluetooth.shade.bleTimeout.label = BLE Timeout
|
||||
thing-type.config.bluetooth.shade.bleTimeout.description = Timeout in seconds for Bluetooth Low Energy operations
|
||||
thing-type.config.bluetooth.shade.heartbeatDelay.label = Heartbeat Interval
|
||||
thing-type.config.bluetooth.shade.heartbeatDelay.description = Interval in seconds for Bluetooth device heart beat checks
|
||||
thing-type.config.bluetooth.shade.pollingDelay.label = Polling Interval
|
||||
thing-type.config.bluetooth.shade.pollingDelay.description = Interval in seconds for polling the battery state
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.bluetooth.primary.label = Position
|
||||
channel-type.bluetooth.primary.description = The vertical position of the shade
|
||||
channel-type.bluetooth.secondary.label = Secondary Position
|
||||
channel-type.bluetooth.secondary.description = The secondary vertical position (on top-down/bottom-up shades)
|
||||
channel-type.bluetooth.tilt.label = Tilt
|
||||
channel-type.bluetooth.tilt.description = The tilt of the slats in the shade
|
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="bluetooth"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Shade Thing Type -->
|
||||
<thing-type id="shade">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="roaming"/>
|
||||
<bridge-type-ref id="bluegiga"/>
|
||||
<bridge-type-ref id="bluez"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>PowerView Shade</label>
|
||||
<description>Hunter Douglas (Luxaflex) PowerView Gen3 Shade</description>
|
||||
|
||||
<channels>
|
||||
<channel id="primary" typeId="primary"/>
|
||||
<channel id="secondary" typeId="secondary"/>
|
||||
<channel id="tilt" typeId="tilt"/>
|
||||
<channel id="battery-level" typeId="system.battery-level"/>
|
||||
<channel id="rssi" typeId="rssi"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="address" type="text" required="true">
|
||||
<label>Address</label>
|
||||
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||
</parameter>
|
||||
<parameter name="pollingDelay" type="integer" min="60">
|
||||
<label>Polling Interval</label>
|
||||
<advanced>true</advanced>
|
||||
<description>Interval in seconds for polling the battery state</description>
|
||||
<default>300</default>
|
||||
</parameter>
|
||||
<parameter name="heartbeatDelay" type="integer" min="5">
|
||||
<label>Heartbeat Interval</label>
|
||||
<advanced>true</advanced>
|
||||
<description>Interval in seconds for Bluetooth device heart beat checks</description>
|
||||
<default>15</default>
|
||||
</parameter>
|
||||
<parameter name="bleTimeout" type="integer" min="1">
|
||||
<label>BLE Timeout</label>
|
||||
<advanced>true</advanced>
|
||||
<description>Timeout in seconds for Bluetooth Low Energy operations</description>
|
||||
<default>6</default>
|
||||
</parameter>
|
||||
<parameter name="encryptionKey" type="text">
|
||||
<label>Encryption Key</label>
|
||||
<description>Encryption key to be used on position commands</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="primary">
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Position</label>
|
||||
<description>The vertical position of the shade</description>
|
||||
<category>Blinds</category>
|
||||
<state min="0" max="100" step="1" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="secondary">
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Secondary Position</label>
|
||||
<description>The secondary vertical position (on top-down/bottom-up shades)</description>
|
||||
<category>Blinds</category>
|
||||
<state min="0" max="100" step="1" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="tilt">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Tilt</label>
|
||||
<description>The tilt of the slats in the shade</description>
|
||||
<category>Blinds</category>
|
||||
<state min="0" max="100" step="1" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.bluetooth.hdpowerview.test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.bluetooth.hdpowerview.internal.shade.ShadeDataWriter;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
|
||||
/**
|
||||
* Test of shade position calculations etc.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class ShadeTests {
|
||||
|
||||
/**
|
||||
* Map of position command values as sniffed during testing with the HD Powerview App. The map keys are the target
|
||||
* position values (range 0..100%) set manually via the App, and the map values are the results sniffed as output
|
||||
* from the App.
|
||||
*/
|
||||
private static final Map<Double, byte[]> HD_POWERVIEW_APP_OBSERVED_RESULTS = new TreeMap<>();
|
||||
static {
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.00, HexFormat.ofDelimiter(":").parseHex("5c:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.01, HexFormat.ofDelimiter(":").parseHex("5d:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.02, HexFormat.ofDelimiter(":").parseHex("5e:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.03, HexFormat.ofDelimiter(":").parseHex("5f:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.04, HexFormat.ofDelimiter(":").parseHex("58:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.05, HexFormat.ofDelimiter(":").parseHex("59:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.06, HexFormat.ofDelimiter(":").parseHex("5a:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.07, HexFormat.ofDelimiter(":").parseHex("5b:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.08, HexFormat.ofDelimiter(":").parseHex("54:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.09, HexFormat.ofDelimiter(":").parseHex("55:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.10, HexFormat.ofDelimiter(":").parseHex("56:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.11, HexFormat.ofDelimiter(":").parseHex("57:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.12, HexFormat.ofDelimiter(":").parseHex("50:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.13, HexFormat.ofDelimiter(":").parseHex("51:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.14, HexFormat.ofDelimiter(":").parseHex("52:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.15, HexFormat.ofDelimiter(":").parseHex("53:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.16, HexFormat.ofDelimiter(":").parseHex("4c:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.17, HexFormat.ofDelimiter(":").parseHex("4d:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.18, HexFormat.ofDelimiter(":").parseHex("4e:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.19, HexFormat.ofDelimiter(":").parseHex("4f:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.20, HexFormat.ofDelimiter(":").parseHex("48:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.30, HexFormat.ofDelimiter(":").parseHex("42:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.40, HexFormat.ofDelimiter(":").parseHex("74:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.50, HexFormat.ofDelimiter(":").parseHex("6e:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.60, HexFormat.ofDelimiter(":").parseHex("60:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.70, HexFormat.ofDelimiter(":").parseHex("1a:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.80, HexFormat.ofDelimiter(":").parseHex("0c:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(0.90, HexFormat.ofDelimiter(":").parseHex("06:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.00, HexFormat.ofDelimiter(":").parseHex("38:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.10, HexFormat.ofDelimiter(":").parseHex("32:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.20, HexFormat.ofDelimiter(":").parseHex("24:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.30, HexFormat.ofDelimiter(":").parseHex("de:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.40, HexFormat.ofDelimiter(":").parseHex("d0:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.50, HexFormat.ofDelimiter(":").parseHex("ca:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.60, HexFormat.ofDelimiter(":").parseHex("fc:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.70, HexFormat.ofDelimiter(":").parseHex("f6:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.80, HexFormat.ofDelimiter(":").parseHex("e8:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(1.90, HexFormat.ofDelimiter(":").parseHex("e2:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(2.00, HexFormat.ofDelimiter(":").parseHex("94:87"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(3.00, HexFormat.ofDelimiter(":").parseHex("70:86"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(4.00, HexFormat.ofDelimiter(":").parseHex("cc:86"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(5.00, HexFormat.ofDelimiter(":").parseHex("a8:86"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(6.00, HexFormat.ofDelimiter(":").parseHex("04:85"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(7.00, HexFormat.ofDelimiter(":").parseHex("e0:85"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(8.00, HexFormat.ofDelimiter(":").parseHex("7c:84"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(9.00, HexFormat.ofDelimiter(":").parseHex("d8:84"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(10.00, HexFormat.ofDelimiter(":").parseHex("b4:84"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(11.00, HexFormat.ofDelimiter(":").parseHex("10:83"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(12.00, HexFormat.ofDelimiter(":").parseHex("ec:83"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(13.00, HexFormat.ofDelimiter(":").parseHex("48:82"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(14.00, HexFormat.ofDelimiter(":").parseHex("24:82"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(15.00, HexFormat.ofDelimiter(":").parseHex("80:82"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(16.00, HexFormat.ofDelimiter(":").parseHex("1c:81"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(17.00, HexFormat.ofDelimiter(":").parseHex("f8:81"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(18.00, HexFormat.ofDelimiter(":").parseHex("54:80"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(19.00, HexFormat.ofDelimiter(":").parseHex("30:80"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(20.00, HexFormat.ofDelimiter(":").parseHex("8c:80"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(20.46, HexFormat.ofDelimiter(":").parseHex("a2:80"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(20.47, HexFormat.ofDelimiter(":").parseHex("a3:80"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(20.48, HexFormat.ofDelimiter(":").parseHex("5c:8f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(20.49, HexFormat.ofDelimiter(":").parseHex("5c:8f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(20.50, HexFormat.ofDelimiter(":").parseHex("5e:8f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(21.00, HexFormat.ofDelimiter(":").parseHex("68:8f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(22.00, HexFormat.ofDelimiter(":").parseHex("c4:8f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(23.00, HexFormat.ofDelimiter(":").parseHex("a0:8f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(24.00, HexFormat.ofDelimiter(":").parseHex("3c:8e"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(25.00, HexFormat.ofDelimiter(":").parseHex("98:8e"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(26.00, HexFormat.ofDelimiter(":").parseHex("74:8d"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(27.00, HexFormat.ofDelimiter(":").parseHex("d0:8d"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(28.00, HexFormat.ofDelimiter(":").parseHex("ac:8d"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(29.00, HexFormat.ofDelimiter(":").parseHex("08:8c"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(30.00, HexFormat.ofDelimiter(":").parseHex("e4:8c"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(31.00, HexFormat.ofDelimiter(":").parseHex("40:8b"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(32.00, HexFormat.ofDelimiter(":").parseHex("dc:8b"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(33.00, HexFormat.ofDelimiter(":").parseHex("b8:8b"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(34.00, HexFormat.ofDelimiter(":").parseHex("14:8a"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(35.00, HexFormat.ofDelimiter(":").parseHex("f0:8a"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(36.00, HexFormat.ofDelimiter(":").parseHex("4c:89"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(37.00, HexFormat.ofDelimiter(":").parseHex("28:89"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(38.00, HexFormat.ofDelimiter(":").parseHex("84:89"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(39.00, HexFormat.ofDelimiter(":").parseHex("60:88"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(40.00, HexFormat.ofDelimiter(":").parseHex("fc:88"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(40.94, HexFormat.ofDelimiter(":").parseHex("a2:88"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(40.95, HexFormat.ofDelimiter(":").parseHex("a3:88"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(40.96, HexFormat.ofDelimiter(":").parseHex("5c:97"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(40.97, HexFormat.ofDelimiter(":").parseHex("5d:97"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(40.98, HexFormat.ofDelimiter(":").parseHex("5e:97"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(41.00, HexFormat.ofDelimiter(":").parseHex("58:97"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(42.00, HexFormat.ofDelimiter(":").parseHex("34:97"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(43.00, HexFormat.ofDelimiter(":").parseHex("90:97"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(44.00, HexFormat.ofDelimiter(":").parseHex("6c:96"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(45.00, HexFormat.ofDelimiter(":").parseHex("c8:96"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(46.00, HexFormat.ofDelimiter(":").parseHex("a4:96"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(47.00, HexFormat.ofDelimiter(":").parseHex("00:95"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(48.00, HexFormat.ofDelimiter(":").parseHex("9c:95"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(49.00, HexFormat.ofDelimiter(":").parseHex("78:94"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(50.00, HexFormat.ofDelimiter(":").parseHex("d4:94"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(51.00, HexFormat.ofDelimiter(":").parseHex("b0:94"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(52.00, HexFormat.ofDelimiter(":").parseHex("0c:93"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(53.00, HexFormat.ofDelimiter(":").parseHex("e8:93"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(54.00, HexFormat.ofDelimiter(":").parseHex("44:92"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(55.00, HexFormat.ofDelimiter(":").parseHex("20:92"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(56.00, HexFormat.ofDelimiter(":").parseHex("bc:92"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(57.00, HexFormat.ofDelimiter(":").parseHex("18:91"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(58.00, HexFormat.ofDelimiter(":").parseHex("f4:91"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(59.00, HexFormat.ofDelimiter(":").parseHex("50:90"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(60.00, HexFormat.ofDelimiter(":").parseHex("2c:90"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(61.00, HexFormat.ofDelimiter(":").parseHex("88:90"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(61.42, HexFormat.ofDelimiter(":").parseHex("a2:90"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(61.43, HexFormat.ofDelimiter(":").parseHex("a3:90"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(61.44, HexFormat.ofDelimiter(":").parseHex("5c:9f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(61.45, HexFormat.ofDelimiter(":").parseHex("5d:9f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(61.46, HexFormat.ofDelimiter(":").parseHex("5e:9f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(62.00, HexFormat.ofDelimiter(":").parseHex("64:9f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(63.00, HexFormat.ofDelimiter(":").parseHex("c0:9f"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(64.00, HexFormat.ofDelimiter(":").parseHex("5c:9e"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(65.00, HexFormat.ofDelimiter(":").parseHex("38:9e"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(66.00, HexFormat.ofDelimiter(":").parseHex("94:9e"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(67.00, HexFormat.ofDelimiter(":").parseHex("70:9d"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(68.00, HexFormat.ofDelimiter(":").parseHex("cc:9d"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(69.00, HexFormat.ofDelimiter(":").parseHex("a8:9d"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(70.00, HexFormat.ofDelimiter(":").parseHex("04:9c"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(71.00, HexFormat.ofDelimiter(":").parseHex("e0:9c"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(72.00, HexFormat.ofDelimiter(":").parseHex("7c:9b"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(73.00, HexFormat.ofDelimiter(":").parseHex("d8:9b"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(74.00, HexFormat.ofDelimiter(":").parseHex("b4:9b"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(75.00, HexFormat.ofDelimiter(":").parseHex("10:9a"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(76.00, HexFormat.ofDelimiter(":").parseHex("ec:9a"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(77.00, HexFormat.ofDelimiter(":").parseHex("48:99"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(78.00, HexFormat.ofDelimiter(":").parseHex("24:99"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(79.00, HexFormat.ofDelimiter(":").parseHex("80:99"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(80.00, HexFormat.ofDelimiter(":").parseHex("1c:98"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(81.00, HexFormat.ofDelimiter(":").parseHex("f8:98"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(81.90, HexFormat.ofDelimiter(":").parseHex("a2:98"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(81.91, HexFormat.ofDelimiter(":").parseHex("a3:98"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(81.92, HexFormat.ofDelimiter(":").parseHex("5c:a7"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(81.93, HexFormat.ofDelimiter(":").parseHex("5d:a7"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(81.94, HexFormat.ofDelimiter(":").parseHex("5e:a7"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(82.00, HexFormat.ofDelimiter(":").parseHex("54:a7"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(83.00, HexFormat.ofDelimiter(":").parseHex("30:a7"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(84.00, HexFormat.ofDelimiter(":").parseHex("8c:a7"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(85.00, HexFormat.ofDelimiter(":").parseHex("68:a6"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(86.00, HexFormat.ofDelimiter(":").parseHex("c4:a6"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(87.00, HexFormat.ofDelimiter(":").parseHex("a0:a6"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(88.00, HexFormat.ofDelimiter(":").parseHex("3c:a5"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(89.00, HexFormat.ofDelimiter(":").parseHex("98:a5"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(90.00, HexFormat.ofDelimiter(":").parseHex("74:a4"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(91.00, HexFormat.ofDelimiter(":").parseHex("d0:a4"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(92.00, HexFormat.ofDelimiter(":").parseHex("ac:a4"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(93.00, HexFormat.ofDelimiter(":").parseHex("08:a3"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(94.00, HexFormat.ofDelimiter(":").parseHex("e4:a3"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(95.00, HexFormat.ofDelimiter(":").parseHex("40:a2"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(96.00, HexFormat.ofDelimiter(":").parseHex("dc:a2"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(97.00, HexFormat.ofDelimiter(":").parseHex("b8:a2"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(98.00, HexFormat.ofDelimiter(":").parseHex("14:a1"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(99.00, HexFormat.ofDelimiter(":").parseHex("f0:a1"));
|
||||
HD_POWERVIEW_APP_OBSERVED_RESULTS.put(100.00, HexFormat.ofDelimiter(":").parseHex("4c:a0"));
|
||||
}
|
||||
|
||||
private static final String TEST_KEY = "02c2efcbd4064d59409c980e627e2fc7"; // (or 9440bf8b334c2b6c8564d80548b67c00)
|
||||
|
||||
/**
|
||||
* Compare the results of the binding {@code ShadeDataWriter} conversions against the results of the HD Powerview
|
||||
* App conversions, as sniffed over the air using a Bluetooth sniffer.
|
||||
*/
|
||||
@Test
|
||||
void testCalculatedEqualsObserved() {
|
||||
for (Entry<Double, byte[]> observedResult : HD_POWERVIEW_APP_OBSERVED_RESULTS.entrySet()) {
|
||||
try {
|
||||
byte[] calculated = new ShadeDataWriter().withPrimary(observedResult.getKey()).getEncrypted(TEST_KEY);
|
||||
byte[] observed = observedResult.getValue();
|
||||
assertEquals(observed[0], calculated[4], 1); // allow error of 1 in LSB for rounding
|
||||
assertEquals(observed[1], calculated[5]);
|
||||
|
||||
} catch (InvalidKeyException | IllegalArgumentException | NoSuchAlgorithmException | NoSuchPaddingException
|
||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that {@code ShadeDataWriter} produces correct values.
|
||||
*/
|
||||
@Test
|
||||
void testShadeDataWriter() {
|
||||
try {
|
||||
String actual;
|
||||
String expected;
|
||||
|
||||
// test basic output
|
||||
actual = HexUtils.bytesToHex(new ShadeDataWriter().getEncrypted(TEST_KEY));
|
||||
expected = "1F70847E5C07AD03100E0FB3DA";
|
||||
assertTrue(expected.equals(actual));
|
||||
|
||||
// test sequence number only
|
||||
actual = HexUtils.bytesToHex(new ShadeDataWriter().withSequence((byte) 1).getEncrypted(TEST_KEY));
|
||||
expected = "1F70857E5C07AD03100E0FB3DA";
|
||||
assertTrue(expected.equals(actual));
|
||||
|
||||
// test primary position only
|
||||
actual = HexUtils.bytesToHex(new ShadeDataWriter().withPrimary(100).getEncrypted(TEST_KEY));
|
||||
expected = "1F70847E4CA0AD03100E0FB3DA";
|
||||
assertTrue(expected.equals(actual));
|
||||
|
||||
// test tilt position only
|
||||
actual = HexUtils.bytesToHex(new ShadeDataWriter().withTilt(40).getEncrypted(TEST_KEY));
|
||||
expected = "1F70847E5C07AD03100E2733DA";
|
||||
assertTrue(expected.equals(actual));
|
||||
|
||||
// test sequence number, plus primary position, plus secondary position
|
||||
expected = "1F70227EE48C4580100E0FB3DA";
|
||||
actual = HexUtils.bytesToHex(new ShadeDataWriter().withSequence((byte) 0xa6).withPrimary(30)
|
||||
.withSecondary(10).getEncrypted(TEST_KEY));
|
||||
assertTrue(expected.equals(actual));
|
||||
|
||||
} catch (InvalidKeyException | IllegalArgumentException | NoSuchAlgorithmException | NoSuchPaddingException
|
||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,4 +8,20 @@
|
||||
<description>This binding supports the Bluetooth protocol.</description>
|
||||
<connection>local</connection>
|
||||
|
||||
<discovery-methods>
|
||||
<discovery-method>
|
||||
<service-type>usb</service-type>
|
||||
<match-properties>
|
||||
<match-property>
|
||||
<name>manufacturer</name>
|
||||
<regex>(?i).*bluegiga.*</regex>
|
||||
</match-property>
|
||||
<match-property>
|
||||
<name>chipId</name>
|
||||
<regex>0258:0001</regex>
|
||||
</match-property>
|
||||
</match-properties>
|
||||
</discovery-method>
|
||||
</discovery-methods>
|
||||
|
||||
</addon:addon>
|
||||
|
@ -24,10 +24,17 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Class containing the database of all known shade 'types' and their respective 'capabilities'.
|
||||
*
|
||||
* <p>
|
||||
* If user systems detect shade types that are not in the database, then this class can issue logger warning messages
|
||||
* indicating such absence, and prompting the user to report it to developers so that the database and the respective
|
||||
* binding functionality can (hopefully) be extended over time.
|
||||
* <p>
|
||||
* <b>NOTA BENE</b>: this database is required by the two bindings listed below. It is maintained here in the former
|
||||
* binding, but it is consumed also by the latter binding. Therefore <b>do NOT delete or modify this file</b> unless you
|
||||
* have carefully checked against regressions in the latter binding.
|
||||
* <li>HD Powerview binding: 'org.openhab.binding.hdpowerview</li>
|
||||
* <li>HD Powerview Bluetooth Low Energy binding: 'org.openhab.binding.bluetooth.hdpowerview</li>
|
||||
* <p>
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial Contribution
|
||||
*/
|
||||
|
@ -83,6 +83,7 @@
|
||||
<module>org.openhab.binding.bluetooth.generic</module>
|
||||
<module>org.openhab.binding.bluetooth.govee</module>
|
||||
<module>org.openhab.binding.bluetooth.grundfosalpha</module>
|
||||
<module>org.openhab.binding.bluetooth.hdpowerview</module>
|
||||
<module>org.openhab.binding.bluetooth.radoneye</module>
|
||||
<module>org.openhab.binding.bluetooth.roaming</module>
|
||||
<module>org.openhab.binding.bluetooth.ruuvitag</module>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.govee/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.grundfosalpha/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.hdpowerview/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.radoneye/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.roaming/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version}</bundle>
|
||||
|
Loading…
Reference in New Issue
Block a user