Skip to content

Commit

Permalink
[bluetooth.hdpowerview] New binding using Bluetooth Low Energy (openh…
Browse files Browse the repository at this point in the history
…ab#17099)

* [bluetooth.hdpowerview] initial contribution

Signed-off-by: AndrewFG <software@whitebear.ch>
  • Loading branch information
andrewfg authored and matchews committed Oct 18, 2024
1 parent 71b1980 commit 4831405
Show file tree
Hide file tree
Showing 19 changed files with 1,627 additions and 1 deletion.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions bundles/org.openhab.binding.bluetooth.hdpowerview/NOTICE
Original file line number Diff line number Diff line change
@@ -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 changes: 81 additions & 0 deletions bundles/org.openhab.binding.bluetooth.hdpowerview/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions bundles/org.openhab.binding.bluetooth.hdpowerview/pom.xml
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 4831405

Please sign in to comment.