[bluetooth.enoceanble] Initial contribution EnOcean BLE Binding (#9223)

* [bluetooth.enoceanble] Initial contribution EnOcean BLE Binding

Signed-off-by: Patrick Fink <mail@pfink.de>

* Put @Nullable annotation inline

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Connor Petty <mistercpp2000@gmail.com>

* [bluetooth.enoceanble] Add binding to CODEOWNERS

Signed-off-by: Patrick Fink <mail@pfink.de>

* [bluetooth.enoceanble] Remove obsolete transport serial feature

Signed-off-by: Patrick Fink <mail@pfink.de>

* [bluetooth.enoceanble] Add binding to footer.xml

Signed-off-by: Patrick Fink <mail@pfink.de>

* [bluetooth.enoceanble] Replace ruuvitag leftovers

Signed-off-by: Patrick Fink <mail@pfink.de>

* [bluetooth.enoceanble] Remove tinyB reference

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Connor Petty <mistercpp2000@gmail.com>
This commit is contained in:
Patrick Fink
2020-12-05 19:11:38 +01:00
committed by GitHub
parent 68dd5569f0
commit 601415db17
14 changed files with 573 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.bluetooth.enoceanble-${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/${project.version}/xml/features</repository>
<feature name="openhab-binding-bluetooth-enoceanble" description="Bluetooth Binding EnOcean BLE" version="${project.version}">
<feature>openhab-runtime-base</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.enoceanble/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.enoceanble.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link EnoceanBleBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Patrick Fink - Initial contribution
*/
@NonNullByDefault
public class EnoceanBleBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_PTM215B = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID,
"ptm215b");
// Channel IDs
public static final String CHANNEL_ID_ROCKER1 = "rocker1";
public static final String CHANNEL_ID_ROCKER2 = "rocker2";
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2020 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.enoceanble.internal;
import java.util.Collections;
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.BluetoothBindingConstants;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This discovery participant is able to recognize enoceanble devices and create discovery results for them.
*
* @author Patrick Fink - Initial contribution
*
*/
@NonNullByDefault
@Component
public class EnoceanBleDiscoveryParticipant implements BluetoothDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(EnoceanBleDiscoveryParticipant.class);
private static final int ENOCEAN_COMPANY_ID = 986;
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(EnoceanBleBindingConstants.THING_TYPE_PTM215B);
}
@Override
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
Integer manufacturerId = device.getManufacturerId();
logger.warn("Discovered device {} with manufacturerId {} and name {}", device.getAddress(), manufacturerId,
device.getName());
if (manufacturerId != null && manufacturerId == ENOCEAN_COMPANY_ID) {
return new ThingUID(EnoceanBleBindingConstants.THING_TYPE_PTM215B, device.getAdapter().getUID(),
device.getAddress().toString().toLowerCase().replace(":", ""));
}
return null;
}
@Override
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
ThingUID thingUID = getThingUID(device);
if (thingUID == null) {
return null;
}
String label = "Enocean PTM215B Rocker";
Map<String, Object> properties = new HashMap<>();
properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
properties.put(Thing.PROPERTY_VENDOR, "EnOcean GmbH");
Integer txPower = device.getTxPower();
if (txPower != null) {
properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
}
// Create the discovery result and add to the inbox
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS)
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 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.enoceanble.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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 EnoceanBleHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Patrick Fink - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.enoceanble")
public class EnoceanBleHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.singleton(EnoceanBleBindingConstants.THING_TYPE_PTM215B);
@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 (thingTypeUID.equals(EnoceanBleBindingConstants.THING_TYPE_PTM215B)) {
return new EnoceanBleRockerHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2020 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.enoceanble.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link EnoceanBlePtm215Event} class is parsing the BLE manufacturer data into an event object.
*
* @author Patrick Fink - Initial contribution
*/
@NonNullByDefault
public class EnoceanBlePtm215Event {
private static final byte PRESSED = 0x1;
private static final byte BUTTON1_DIR1 = 0x10;
private static final byte BUTTON1_DIR2 = 0x8;
private static final byte BUTTON2_DIR1 = 0x4;
private static final byte BUTTON2_DIR2 = 0x2;
private final byte byteState;
public EnoceanBlePtm215Event(byte[] manufacturerData) {
byteState = manufacturerData[6];
}
public boolean isPressed() {
return checkFlag(PRESSED);
}
public boolean isButton1() {
return checkFlag(BUTTON1_DIR1) || checkFlag(BUTTON1_DIR2);
}
public boolean isButton2() {
return checkFlag(BUTTON2_DIR1) || checkFlag(BUTTON2_DIR2);
}
public boolean isDir1() {
return checkFlag(BUTTON1_DIR1) || checkFlag(BUTTON2_DIR1);
}
public boolean isDir2() {
return checkFlag(BUTTON1_DIR2) || checkFlag(BUTTON2_DIR2);
}
private boolean checkFlag(int flag) {
return (byteState & flag) == flag;
}
@Override
public String toString() {
return "Button " + (isButton1() ? 1 : 2) + " Dir " + (isDir1() ? 1 : 2) + " "
+ (isPressed() ? "PRESSED" : "RELEASED");
}
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2010-2020 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.enoceanble.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
import org.openhab.core.thing.CommonTriggerEvents;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EnoceanBleRockerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Patrick Fink - Initial contribution
*/
@NonNullByDefault
public class EnoceanBleRockerHandler extends BeaconBluetoothHandler {
private final Logger logger = LoggerFactory.getLogger(EnoceanBleRockerHandler.class);
public EnoceanBleRockerHandler(Thing thing) {
super(thing);
}
@Override
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
super.onScanRecordReceived(scanNotification);
final byte[] manufacturerData = scanNotification.getManufacturerData();
logger.debug("Received new scan notification for {}: {}", device.getAddress(), manufacturerData);
try {
if (manufacturerData != null && manufacturerData.length > 0) {
EnoceanBlePtm215Event event = new EnoceanBlePtm215Event(manufacturerData);
logger.debug("Parsed manufacturer data to PTM215B event: {}", event);
triggerChannel(resolveChannel(event), resolveTriggerEvent(event));
}
} catch (IllegalStateException e) {
logger.warn("PTM215B event could not be parsed correctly, exception occured:", e);
}
}
protected String resolveChannel(EnoceanBlePtm215Event event) {
if (event.isButton1()) {
return EnoceanBleBindingConstants.CHANNEL_ID_ROCKER1;
}
if (event.isButton2()) {
return EnoceanBleBindingConstants.CHANNEL_ID_ROCKER2;
}
throw new IllegalStateException(
"PTM215B event cannot be resolved correctly to a channel, probably received message is invalid");
}
protected String resolveTriggerEvent(EnoceanBlePtm215Event event) {
if (event.isDir1()) {
if (event.isPressed()) {
return CommonTriggerEvents.DIR1_PRESSED;
} else {
return CommonTriggerEvents.DIR1_RELEASED;
}
}
if (event.isDir2()) {
if (event.isPressed()) {
return CommonTriggerEvents.DIR2_PRESSED;
} else {
return CommonTriggerEvents.DIR2_RELEASED;
}
}
throw new IllegalStateException(
"PTM215B event cannot be resolved correctly to an openHAB event, probably received message is invalid");
}
}

View File

@@ -0,0 +1,30 @@
<?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">
<thing-type id="ptm215b">
<supported-bridge-type-refs>
<bridge-type-ref id="roaming"/>
<bridge-type-ref id="bluegiga"/>
<bridge-type-ref id="bluez"/>
</supported-bridge-type-refs>
<label>Enocean PTM 215B Rocker</label>
<description>An Enocean BLE Rocker (PTM 215B)</description>
<channels>
<channel id="rocker1" typeId="system.rawrocker"/>
<channel id="rocker2" typeId="system.rawrocker"/>
</channels>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2020 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.enoceanble.internal;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* {@link EnoceanBlePtm215EventTest} is testing the {@link EnoceanBlePtm215Event} class
* using some sample manufacturerData
*
* @author Patrick Fink - Initial contribution
*/
@NonNullByDefault
public class EnoceanBlePtm215EventTest {
@Test
public void testButton1Dir1Pressed() {
byte[] button1Dir2PressedBytes = { -38, 3, 53, 5, 0, 0, 17, 54, 81, 61, 54 };
EnoceanBlePtm215Event button1Dir2PressedEvent = new EnoceanBlePtm215Event(button1Dir2PressedBytes);
assertTrue(button1Dir2PressedEvent.isPressed());
assertTrue(button1Dir2PressedEvent.isButton1());
assertTrue(button1Dir2PressedEvent.isDir1());
}
@Test
public void testButton1Dir1Released() {
byte[] button1Dir2ReleasedBytes = { -38, 3, 54, 5, 0, 0, 16, -83, -25, 103, 3 };
EnoceanBlePtm215Event button1Dir2ReleasedEvent = new EnoceanBlePtm215Event(button1Dir2ReleasedBytes);
assertFalse(button1Dir2ReleasedEvent.isPressed());
assertTrue(button1Dir2ReleasedEvent.isButton1());
assertTrue(button1Dir2ReleasedEvent.isDir1());
}
@Test
public void testButton1Dir2Pressed() {
byte[] button1Dir2PressedBytes = { -38, 3, -97, 32, 0, 0, 9, -99, -15, 16, -74 };
EnoceanBlePtm215Event button1Dir2PressedEvent = new EnoceanBlePtm215Event(button1Dir2PressedBytes);
assertTrue(button1Dir2PressedEvent.isPressed());
assertTrue(button1Dir2PressedEvent.isButton1());
assertTrue(button1Dir2PressedEvent.isDir2());
}
@Test
public void testButton1Dir2Released() {
byte[] button1Dir2ReleasedBytes = { -38, 3, -96, 32, 0, 0, 8, -87, -96, 98, 74 };
EnoceanBlePtm215Event button1Dir2ReleasedEvent = new EnoceanBlePtm215Event(button1Dir2ReleasedBytes);
assertFalse(button1Dir2ReleasedEvent.isPressed());
assertTrue(button1Dir2ReleasedEvent.isButton1());
assertTrue(button1Dir2ReleasedEvent.isDir2());
}
@Test
public void testButton2Dir1Pressed() {
byte[] button2Dir1PressedBytes = { -38, 3, -91, 32, 0, 0, 5, 89, 104, 24, -67 };
EnoceanBlePtm215Event button2Dir1PressedEvent = new EnoceanBlePtm215Event(button2Dir1PressedBytes);
assertTrue(button2Dir1PressedEvent.isPressed());
assertTrue(button2Dir1PressedEvent.isButton2());
assertTrue(button2Dir1PressedEvent.isDir1());
}
@Test
public void testButton2Dir1Released() {
byte[] button2Dir1ReleasedBytes = { -38, 3, -90, 32, 0, 0, 4, 120, 52, -115, 61 };
EnoceanBlePtm215Event button2Dir1ReleasedEvent = new EnoceanBlePtm215Event(button2Dir1ReleasedBytes);
assertFalse(button2Dir1ReleasedEvent.isPressed());
assertTrue(button2Dir1ReleasedEvent.isButton2());
assertTrue(button2Dir1ReleasedEvent.isDir1());
}
@Test
public void testButton2Dir2Pressed() {
byte[] button2Dir2PressedBytes = { -38, 3, -95, 32, 0, 0, 3, -105, -33, 62, 45 };
EnoceanBlePtm215Event button2Dir1PressedEvent = new EnoceanBlePtm215Event(button2Dir2PressedBytes);
assertTrue(button2Dir1PressedEvent.isPressed());
assertTrue(button2Dir1PressedEvent.isButton2());
assertTrue(button2Dir1PressedEvent.isDir2());
}
@Test
public void testButton2Dir2Released() {
byte[] button2Dir2ReleasedBytes = { -38, 3, -94, 32, 0, 0, 2, 59, 118, 52, -69 };
EnoceanBlePtm215Event button2Dir2ReleasedEvent = new EnoceanBlePtm215Event(button2Dir2ReleasedBytes);
assertFalse(button2Dir2ReleasedEvent.isPressed());
assertTrue(button2Dir2ReleasedEvent.isButton2());
assertTrue(button2Dir2ReleasedEvent.isDir2());
}
}