mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[bluetooth.listener] Initial binding contribution (#18733)
* Initial bluetooth.listener binding contribution. Signed-off-by: docbender <vita.tucek@seznam.cz>
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.grundfosalpha/ @tisoft @jlaur
|
||||
/bundles/org.openhab.binding.bluetooth.hdpowerview/ @andrewfg
|
||||
/bundles/org.openhab.binding.bluetooth.listener/ @docbender
|
||||
/bundles/org.openhab.binding.bluetooth.radoneye/ @petero-dk
|
||||
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
|
||||
|
||||
@@ -256,6 +256,11 @@
|
||||
<artifactId>org.openhab.binding.bluetooth.grundfosalpha</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth.listener</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth.radoneye</artifactId>
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,117 @@
|
||||
# Listener Binding
|
||||
|
||||
Bluetooth extension that supports receiving broadcasted data.
|
||||
These are manufacturer and service data broadcasted from Bluetooth device.
|
||||
It is therefore possible to receive data from Bluetooth device without connecting to them or from non-connectable device.
|
||||
This makes it different from other bindings that require a two-way connection for communication or are designed only for a specific device.
|
||||
Listener binding can work with any device that spontaneously advertise its data.
|
||||
To obtain data from such a device, however, it is necessary to know what device provides the data, respectively what services the device has implemented.
|
||||
The nRF Connect Android app is ideal for debugging the data transmitted by the device and detecting the transmitted service UUIDs.
|
||||
|
||||
## Supported Things
|
||||
|
||||
| Thing Type ID | Description |
|
||||
| --------------- | -------------------------- |
|
||||
| listener | Bluetooth broadcast device |
|
||||
|
||||
The binding uses one of the Bluetooth bridges, which needs to be configured first.
|
||||
|
||||
## Discovery
|
||||
|
||||
No discovery is implemented.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|---------------------|----------|---------|--------------------------------------------------------------------------------|
|
||||
| address | yes | | The address of the bluetooth device (in format "XX:XX:XX:XX:XX:XX") |
|
||||
| changeByteOrder | no | false | For number channels with length 2, 4 or 8 bytes byte order could be changed. Default (false) order is little endian. |
|
||||
| autoChannelCreation | no | false | Enable automatic channel creation from received service and manufacturer data |
|
||||
| dataTimeout | no | 1 | Maximum time in minutes before a communication error is raised |
|
||||
|
||||
## Channels
|
||||
|
||||
All channels are read-only.
|
||||
|
||||
| Channel | Type | Read/Write | Description | User configurable |
|
||||
|---------------------|--------|------------|-----------------------------|-------------------|
|
||||
| rssi | Number | R | Signal strength | N |
|
||||
| advertise-interval | Number | R | Advertising interval | N |
|
||||
| manufacturer-number | Number | R | Manufacturer numeric data | Y |
|
||||
| manufacturer-raw | String | R | Manufacturer raw data | Y |
|
||||
| service-number | Number | R | Service numeric data | Y |
|
||||
| service-raw | String | R | Service raw data | Y |
|
||||
|
||||
### Manufacturer-number channels
|
||||
|
||||
Number value is returned.
|
||||
It is be based on Bluetooth manufacturer data.
|
||||
Manufacturer data is specific data that could advertise Bluetooth device.
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---------------------|---------|-------------|
|
||||
| dataBegin | | Data begin in raw data byte array |
|
||||
| dataLength | 1 | Data length. Only values 1, 2, 4, 8 are allowed (for datatypes byte, short, integer and long respectively) |
|
||||
| multiplyer | 1 | Simple data conversion by value multiplication |
|
||||
| payloadLength | 0 | Expected payload length. Default is 0 that means no payload length check is provided. Otherwise only payload with specified length will be accepted |
|
||||
|
||||
### Manufacturer-raw channels
|
||||
|
||||
String value is returned.
|
||||
Channel value represents all received data as string value.
|
||||
It is based on Bluetooth manufacturer data.
|
||||
Manufacturer data specific data that could advertise Bluetooth device.
|
||||
It could be used to debug Bluetooth data.
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---------------------|---------|-------------|
|
||||
| dataBegin | | Data begin in raw data byte array |
|
||||
| dataLength | 1 | Data length. If set to 0 the data will be loaded till the end |
|
||||
|
||||
### Service-number channels
|
||||
|
||||
Number value is returned.
|
||||
It is be based on Bluetooth service data.
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---------------------|---------|--------------|
|
||||
| uuid | | UUID of the Bluetooth service |
|
||||
| dataBegin | | Data begin in raw data byte array |
|
||||
| dataLength | 1 | Data length. Only values 1, 2, 4, 8 are allowed (for datatypes byte, short, integer and long respectively) |
|
||||
| multiplyer | 1 | Simple data conversion by value multiplication |
|
||||
| payloadLength | 0 | Expected payload length. Default is 0 that means no payload length check is provided. Otherwise only payload with specified length will be accepted |
|
||||
|
||||
### Service-raw channels
|
||||
|
||||
String value is returned.
|
||||
Channel value represents all received data as string value.
|
||||
It is based on Bluetooth service data.
|
||||
It could be used to debug Bluetooth data.
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---------------------|---------|--------------|
|
||||
| uuid | | UUID of the Bluetooth service |
|
||||
| dataBegin | | Data begin in raw data byte array |
|
||||
| dataLength | 1 | Data length. If set to 0 the data will be loaded till the end |
|
||||
|
||||
## Full Example
|
||||
|
||||
### `demo.things` Example
|
||||
|
||||
```java
|
||||
Bridge bluetooth:bluez:myBridge [ address="00:00:00:00:00:00" ]
|
||||
Thing bluetooth:listener:myBridge:myDevice (bluetooth:bluez:myBridge) [ address="11:11:11:11:11:11", changeByteOrder="false" ] {
|
||||
Channels:
|
||||
Type service-number : temperature [ dataBegin="6", orderBigEndian="true", payloadLength="13", multiplyer="0.1", uuid="181a", dataLength="2" ]
|
||||
Type service-number : humidity [ dataBegin="8", orderBigEndian="true", payloadLength="13", multiplyer="0.1", uuid="181a", dataLength="2" ]
|
||||
Type service-raw : servicedata [ uuid="181a", dataBegin="0", dataLength="0" ]
|
||||
}
|
||||
```
|
||||
|
||||
### `demo.items` Example
|
||||
|
||||
```java
|
||||
Number temperature "Temperature" { channel="bluetooth:listener:myBridge:myDevice:temperature" }
|
||||
Number humidity "Humidity" { channel="bluetooth:listener:myBridge:myDevice:humidity" }
|
||||
Number serviceData "Service 181a" { channel="bluetooth:listener:myBridge:myDevice:servicedata" }
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.bluetooth.listener</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Bluetooth Listener Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.bluetooth.listener-${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-listener" description="Bluetooth Listener Binding" 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="81">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.listener/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.listener.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link ListenerBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author VitaTucek - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ListenerBindingConstants {
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_LISTENER = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID,
|
||||
"listener");
|
||||
|
||||
// List of Channel ids
|
||||
public static final String CHANNEL_TYPE_INTERVAL = "interval";
|
||||
public static final String CHANNEL_TYPE_MANUFACTURER_NUMBER = "manufacturer-number";
|
||||
public static final String CHANNEL_TYPE_MANUFACTURER_RAW = "manufacturer-raw";
|
||||
public static final String CHANNEL_TYPE_SERVICE_NUMBER = "service-number";
|
||||
public static final String CHANNEL_TYPE_SERVICE_RAW = "service-raw";
|
||||
|
||||
// List of Parameters
|
||||
public static final String PARAMETER_DATA_UUID = "uuid";
|
||||
public static final String PARAMETER_DATA_BEGIN = "dataBegin";
|
||||
public static final String PARAMETER_DATA_LENGTH = "dataLength";
|
||||
public static final String PARAMETER_MULTIPLYER = "multiplyer";
|
||||
public static final String PARAMETER_PAYLOAD_LENGTH = "payloadLength";
|
||||
|
||||
// List of Property IDs
|
||||
public static final String PROPERTY_ADVERTISING_INTERVAL = "advertisingInterval";
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.listener.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link ListenerConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author VitaTucek - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ListenerConfiguration {
|
||||
/**
|
||||
* Device address
|
||||
*/
|
||||
public String address = "";
|
||||
/**
|
||||
* Create raw discovered channels automatically
|
||||
*/
|
||||
public boolean autoChannelCreation = false;
|
||||
/**
|
||||
* Byte order of the numeric channels
|
||||
*/
|
||||
public boolean changeByteOrder = false;
|
||||
/**
|
||||
* Time in minutes before device timeout is set
|
||||
*/
|
||||
public int dataTimeout = 1;
|
||||
}
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.listener.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import javax.measure.MetricPrefix;
|
||||
import javax.measure.quantity.Time;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
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.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ListenerHandler} is responsible for the transformation of the received data.
|
||||
*
|
||||
* @author VitaTucek - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ListenerHandler extends BeaconBluetoothHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(ListenerHandler.class);
|
||||
private final AtomicBoolean receivedStatus = new AtomicBoolean();
|
||||
private @Nullable ListenerConfiguration config;
|
||||
private @Nullable ScheduledFuture<?> heartbeatFuture;
|
||||
private long scanTime = 0;
|
||||
|
||||
public ListenerHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
super.initialize();
|
||||
ListenerConfiguration config = this.config = getConfigAs(ListenerConfiguration.class);
|
||||
// check configuration
|
||||
int timeout = config != null ? config.dataTimeout : 1;
|
||||
// set unknown
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
// start heartbeat job
|
||||
heartbeatFuture = scheduler.scheduleWithFixedDelay(this::heartbeat, 0, timeout, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check device connection timeout
|
||||
*/
|
||||
private void heartbeat() {
|
||||
ListenerConfiguration config = this.config;
|
||||
if (!receivedStatus.getAndSet(false) && getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
|
||||
"@text/offline.communication-error [\"" + (config != null ? config.dataTimeout : "null") + "\"]");
|
||||
scanTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> heartbeatFuture = this.heartbeatFuture;
|
||||
if (heartbeatFuture != null) {
|
||||
heartbeatFuture.cancel(true);
|
||||
this.heartbeatFuture = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
||||
receivedStatus.set(true);
|
||||
super.onScanRecordReceived(scanNotification);
|
||||
// RSSI information
|
||||
processRssiData(scanNotification);
|
||||
// manufacturer data
|
||||
processManufacturerData(scanNotification);
|
||||
// services
|
||||
processServiceData(scanNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract service data if received and coresponding service channel is configured
|
||||
*
|
||||
* @param scanNotification Scan packet information
|
||||
*/
|
||||
private void processServiceData(BluetoothScanNotification scanNotification) {
|
||||
for (var service : scanNotification.getServiceData().entrySet()) {
|
||||
ListenerConfiguration config = this.config;
|
||||
logger.debug("Thing {}: Service notification UUID/value={}/0x{}", getThing().getLabel(), service.getKey(),
|
||||
HexUtils.bytesToHex(service.getValue()));
|
||||
|
||||
String uuid = service.getKey();
|
||||
byte[] data = service.getValue();
|
||||
|
||||
if (uuid.endsWith("-0000-1000-8000-00805f9b34fb")) {
|
||||
if (uuid.startsWith("0000")) {
|
||||
uuid = uuid.substring(4, 8);
|
||||
} else {
|
||||
uuid = uuid.substring(0, 8);
|
||||
}
|
||||
} else if (uuid.endsWith("-8000-00805f9b34fb")) {
|
||||
uuid = uuid.substring(0, 18);
|
||||
}
|
||||
for (var channel : getThing().getChannels()) {
|
||||
ChannelTypeUID channelType = channel.getChannelTypeUID();
|
||||
if (channelType == null) {
|
||||
continue;
|
||||
}
|
||||
// looking for service channel
|
||||
if (!channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_SERVICE_NUMBER)
|
||||
&& !channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_SERVICE_RAW)) {
|
||||
continue;
|
||||
}
|
||||
// get channel properties
|
||||
var properties = channel.getConfiguration().getProperties();
|
||||
// get UUID property
|
||||
var channelUuid = properties.get(ListenerBindingConstants.PARAMETER_DATA_UUID);
|
||||
// looking for channel with advertised UUID
|
||||
if (!channelUuid.toString().equalsIgnoreCase(uuid)) {
|
||||
continue;
|
||||
}
|
||||
// retrieve rest of properties
|
||||
var index = properties.get(ListenerBindingConstants.PARAMETER_DATA_BEGIN);
|
||||
var datalength = properties.get(ListenerBindingConstants.PARAMETER_DATA_LENGTH);
|
||||
var multiplyer = properties.get(ListenerBindingConstants.PARAMETER_MULTIPLYER);
|
||||
var payloadLength = properties.get(ListenerBindingConstants.PARAMETER_PAYLOAD_LENGTH);
|
||||
int indexInt = (index == null) ? 0 : ((BigDecimal) index).intValue();
|
||||
int lengthInt = (datalength == null) ? 2 : ((BigDecimal) datalength).intValue();
|
||||
float multiplyerFloat = (multiplyer == null) ? 1.0f : ((BigDecimal) multiplyer).floatValue();
|
||||
int payloadLengthInt = (payloadLength == null) ? 0 : ((BigDecimal) payloadLength).intValue();
|
||||
// get data for number configured channel
|
||||
if (channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_SERVICE_NUMBER)) {
|
||||
if (lengthInt != 1 && lengthInt != 2 && lengthInt != 4 && lengthInt != 8) {
|
||||
logger.warn("Thing {}: Channel '{}' has set unsupported data length to {}",
|
||||
getThing().getLabel(), channel.getUID().getId(), lengthInt);
|
||||
continue;
|
||||
}
|
||||
if (payloadLengthInt > 0 && payloadLengthInt != data.length) {
|
||||
logger.debug(
|
||||
"Thing {}, Channel {}: Service payload length {}B is different from expected lenght {}B",
|
||||
getThing().getLabel(), channel.getUID().getId(), data.length, payloadLengthInt);
|
||||
continue;
|
||||
}
|
||||
DecimalType value = getDecimalValue(data, indexInt, lengthInt, multiplyerFloat,
|
||||
config != null ? config.changeByteOrder : false);
|
||||
if (value != null) {
|
||||
updateState(channel.getUID(), value);
|
||||
}
|
||||
// get raw data for configured channel
|
||||
} else if (channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_SERVICE_RAW)) {
|
||||
StringType value = getRawValue(data, indexInt, lengthInt);
|
||||
updateState(channel.getUID(), value);
|
||||
}
|
||||
}
|
||||
// create channel if creation is enabled
|
||||
if (config != null && config.autoChannelCreation) {
|
||||
ChannelUID channelUID = new ChannelUID(getThing().getUID(), "service-".concat(uuid));
|
||||
ThingBuilder builder = editThing();
|
||||
if (getThing().getChannel(channelUID) == null) {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put(ListenerBindingConstants.PARAMETER_DATA_UUID, uuid);
|
||||
configuration.put(ListenerBindingConstants.PARAMETER_DATA_BEGIN, 0);
|
||||
configuration.put(ListenerBindingConstants.PARAMETER_DATA_LENGTH, 0);
|
||||
var channel = ChannelBuilder.create(channelUID, "String")
|
||||
.withType(new ChannelTypeUID(BluetoothBindingConstants.BINDING_ID,
|
||||
ListenerBindingConstants.CHANNEL_TYPE_SERVICE_RAW))
|
||||
.withLabel("UUID " + uuid).withDescription("Service raw data")
|
||||
.withConfiguration(configuration).build();
|
||||
builder.withChannel(channel);
|
||||
updateThing(builder.build());
|
||||
updateState(channelUID, new StringType("0x" + HexUtils.bytesToHex(data)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Manufacturer data if received and manufacturer channel is configured
|
||||
*
|
||||
* @param scanNotification Scan packet information
|
||||
*/
|
||||
private void processManufacturerData(BluetoothScanNotification scanNotification) {
|
||||
if (scanNotification.getManufacturerData().length != 0) {
|
||||
ListenerConfiguration config = this.config;
|
||||
// assign manufacturer data
|
||||
byte[] data = scanNotification.getManufacturerData();
|
||||
|
||||
for (var channel : getThing().getChannels()) {
|
||||
ChannelTypeUID channelType = channel.getChannelTypeUID();
|
||||
if (channelType == null) {
|
||||
continue;
|
||||
}
|
||||
// looking for service channel
|
||||
if (!channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_MANUFACTURER_NUMBER)
|
||||
&& !channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_MANUFACTURER_RAW)) {
|
||||
continue;
|
||||
}
|
||||
// get channel properties
|
||||
var properties = channel.getConfiguration().getProperties();
|
||||
// retrieve properties values
|
||||
var index = properties.get(ListenerBindingConstants.PARAMETER_DATA_BEGIN);
|
||||
var datalength = properties.get(ListenerBindingConstants.PARAMETER_DATA_LENGTH);
|
||||
var multiplyer = properties.get(ListenerBindingConstants.PARAMETER_MULTIPLYER);
|
||||
var payloadLength = properties.get(ListenerBindingConstants.PARAMETER_PAYLOAD_LENGTH);
|
||||
int indexInt = (index == null) ? 0 : ((BigDecimal) index).intValue();
|
||||
int lengthInt = (datalength == null) ? 2 : ((BigDecimal) datalength).intValue();
|
||||
float multiplyerFloat = (multiplyer == null) ? 1.0f : ((BigDecimal) multiplyer).floatValue();
|
||||
int payloadLengthInt = (payloadLength == null) ? 0 : ((BigDecimal) payloadLength).intValue();
|
||||
// get data for number configured channel
|
||||
if (channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_MANUFACTURER_NUMBER)) {
|
||||
if (lengthInt != 1 && lengthInt != 2 && lengthInt != 4 && lengthInt != 8) {
|
||||
logger.warn("Thing {}: Channel '{}' has set unsupported data length to {}",
|
||||
getThing().getLabel(), channel.getUID().getId(), lengthInt);
|
||||
continue;
|
||||
}
|
||||
if (payloadLengthInt > 0 && payloadLengthInt != data.length) {
|
||||
logger.debug(
|
||||
"Thing {}, Channel {}: Manufacturer data payload length {}B is different from expected lenght {}B",
|
||||
getThing().getLabel(), channel.getUID().getId(), data.length, payloadLengthInt);
|
||||
continue;
|
||||
}
|
||||
DecimalType value = getDecimalValue(data, indexInt, lengthInt, multiplyerFloat,
|
||||
config != null ? config.changeByteOrder : false);
|
||||
if (value != null) {
|
||||
updateState(channel.getUID(), value);
|
||||
}
|
||||
// get raw data for configured channel
|
||||
} else if (channelType.getId().equals(ListenerBindingConstants.CHANNEL_TYPE_MANUFACTURER_RAW)) {
|
||||
StringType value = getRawValue(data, indexInt, lengthInt);
|
||||
updateState(channel.getUID(), value);
|
||||
}
|
||||
}
|
||||
if (config != null && config.autoChannelCreation) {
|
||||
ChannelUID channelUID = new ChannelUID(getThing().getUID(), "manufacturer-data");
|
||||
ThingBuilder builder = editThing();
|
||||
if (getThing().getChannel(channelUID) == null) {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put(ListenerBindingConstants.PARAMETER_DATA_BEGIN, 0);
|
||||
configuration.put(ListenerBindingConstants.PARAMETER_DATA_LENGTH, 0);
|
||||
var channel = ChannelBuilder.create(channelUID, "String")
|
||||
.withType(new ChannelTypeUID(BluetoothBindingConstants.BINDING_ID,
|
||||
ListenerBindingConstants.CHANNEL_TYPE_MANUFACTURER_RAW))
|
||||
.withLabel("Manufacturer raw data").withConfiguration(configuration).build();
|
||||
builder.withChannel(channel);
|
||||
updateThing(builder.build());
|
||||
}
|
||||
updateState(channelUID,
|
||||
new StringType("0x" + HexUtils.bytesToHex(scanNotification.getManufacturerData())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract RSSI data from nofitication if exists
|
||||
*
|
||||
* @param scanNotification Scan packet information
|
||||
*/
|
||||
private void processRssiData(BluetoothScanNotification scanNotification) {
|
||||
// RSSI boundaries - not all notifications report RSSI value
|
||||
if (scanNotification.getRssi() > Integer.MIN_VALUE) {
|
||||
if (scanTime > 0) {
|
||||
long period = System.currentTimeMillis() - scanTime;
|
||||
updateProperty(ListenerBindingConstants.PROPERTY_ADVERTISING_INTERVAL, String.valueOf(period));
|
||||
QuantityType<Time> quantity = new QuantityType<Time>(period, Units.SECOND.prefix(MetricPrefix.MILLI));
|
||||
updateState(ListenerBindingConstants.CHANNEL_TYPE_INTERVAL, quantity);
|
||||
}
|
||||
scanTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract decimal value from byte array
|
||||
*
|
||||
* @param data source array
|
||||
* @param index start index
|
||||
* @param length length of data
|
||||
* @param multiplyer result correct factor
|
||||
* @param binEndian byte order
|
||||
* @return decimal value
|
||||
*/
|
||||
private @Nullable DecimalType getDecimalValue(byte[] data, int index, int length, float multiplyer,
|
||||
boolean binEndian) {
|
||||
if (data.length < index + length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long value = 0;
|
||||
var buffer = ByteBuffer.wrap(data, index, length);
|
||||
if (binEndian) {
|
||||
buffer = buffer.order(ByteOrder.BIG_ENDIAN);
|
||||
} else {
|
||||
buffer = buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
if (length == 1) {
|
||||
value = buffer.get();
|
||||
} else if (length == 2) {
|
||||
value = buffer.getShort();
|
||||
} else if (length == 4) {
|
||||
value = buffer.getInt();
|
||||
} else if (length == 8) {
|
||||
value = buffer.getLong();
|
||||
}
|
||||
|
||||
if (multiplyer != 1.0f) {
|
||||
return new DecimalType(value * multiplyer);
|
||||
} else {
|
||||
return new DecimalType(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert byte array into string
|
||||
*
|
||||
* @param data source array
|
||||
* @param index start index
|
||||
* @param length length of data
|
||||
* @return string representation of data
|
||||
*/
|
||||
private StringType getRawValue(byte[] data, int index, int length) {
|
||||
int indexTo = data.length;
|
||||
if (length > 0) {
|
||||
indexTo = index + length;
|
||||
}
|
||||
return new StringType("0x" + HexUtils.bytesToHex(Arrays.copyOfRange(data, index, indexTo)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
super.onServicesDiscovered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
super.onCharacteristicUpdate(characteristic, value);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.listener.internal;
|
||||
|
||||
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 ListenerHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author VitaTucek - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.bluetooth.listener", service = ThingHandlerFactory.class)
|
||||
public class ListenerHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set
|
||||
.of(ListenerBindingConstants.THING_TYPE_LISTENER);
|
||||
|
||||
@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(ListenerBindingConstants.THING_TYPE_LISTENER)) {
|
||||
return new ListenerHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
# thing types
|
||||
|
||||
thing-type.bluetooth.listener.label = Listener
|
||||
thing-type.bluetooth.listener.description = Bluetooth Listener
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.bluetooth.listener.address.label = Address
|
||||
thing-type.config.bluetooth.listener.address.description = Bluetooth address in XX:XX:XX:XX:XX:XX format
|
||||
thing-type.config.bluetooth.listener.autoChannelCreation.label = Automatic Channel Creation
|
||||
thing-type.config.bluetooth.listener.autoChannelCreation.description = Enable automatic channel creation from received service and manufacturer data
|
||||
thing-type.config.bluetooth.listener.changeByteOrder.label = Big Endian
|
||||
thing-type.config.bluetooth.listener.changeByteOrder.description = Multibyte item byte order for number channels. For items with length 2, 4 or 8 bytes byte order could be changed. Default (false) order is little endian. For big endian order set this to true.
|
||||
thing-type.config.bluetooth.listener.dataTimeout.label = Data Timeout
|
||||
thing-type.config.bluetooth.listener.dataTimeout.description = Maximum time in minutes before a communication error is raised
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.bluetooth.advertise-interval.label = Advertising Interval
|
||||
channel-type.bluetooth.advertise-interval.description = Advertising interval
|
||||
channel-type.bluetooth.manufacturer-number.label = Manufacturer Numeric Data
|
||||
channel-type.bluetooth.manufacturer-number.description = Manufacturer numeric data
|
||||
channel-type.bluetooth.manufacturer-raw.label = Manufacturer Raw Data
|
||||
channel-type.bluetooth.manufacturer-raw.description = Manufacturer raw data
|
||||
channel-type.bluetooth.service-number.label = Service Numeric Data
|
||||
channel-type.bluetooth.service-number.description = Service numeric data
|
||||
channel-type.bluetooth.service-raw.label = Service Raw Data
|
||||
channel-type.bluetooth.service-raw.description = Service raw data
|
||||
|
||||
# channel types config
|
||||
|
||||
channel-type.config.bluetooth.manufacturer-number.dataBegin.label = Data Begin
|
||||
channel-type.config.bluetooth.manufacturer-number.dataBegin.description = Data begin in the raw data byte array.
|
||||
channel-type.config.bluetooth.manufacturer-number.dataLength.label = Data Length
|
||||
channel-type.config.bluetooth.manufacturer-number.dataLength.description = Data length. Only values 1, 2, 4, 8 are allowed (for datatypes byte, short, integer and long respectively).
|
||||
channel-type.config.bluetooth.manufacturer-number.multiplyer.label = Multiplyer
|
||||
channel-type.config.bluetooth.manufacturer-number.multiplyer.description = Simple data conversion by value multiplication.
|
||||
channel-type.config.bluetooth.manufacturer-number.payloadLength.label = Payload Length
|
||||
channel-type.config.bluetooth.manufacturer-number.payloadLength.description = Expected payload length. Default is 0 that means no payload length check is provided. Otherwise only payload with specified length will be accepted.
|
||||
channel-type.config.bluetooth.manufacturer-raw.dataBegin.label = Data begin
|
||||
channel-type.config.bluetooth.manufacturer-raw.dataBegin.description = Data begin in the raw data byte array.
|
||||
channel-type.config.bluetooth.manufacturer-raw.dataLength.label = Data Length
|
||||
channel-type.config.bluetooth.manufacturer-raw.dataLength.description = Data length. If set to 0 the data will be loaded till the end.
|
||||
channel-type.config.bluetooth.service-number.dataBegin.label = Data Begin
|
||||
channel-type.config.bluetooth.service-number.dataBegin.description = Data begin in the raw data byte array.
|
||||
channel-type.config.bluetooth.service-number.dataLength.label = Data Length
|
||||
channel-type.config.bluetooth.service-number.dataLength.description = Data length. Only values 1, 2, 4, 8 are allowed (for datatypes byte, short, integer and long respectively).
|
||||
channel-type.config.bluetooth.service-number.multiplyer.label = Multiplyer
|
||||
channel-type.config.bluetooth.service-number.multiplyer.description = Simple data conversion by value multiplication.
|
||||
channel-type.config.bluetooth.service-number.payloadLength.label = Payload Length
|
||||
channel-type.config.bluetooth.service-number.payloadLength.description = Expected payload length. Default is 0 that means no payload length check is provided. Otherwise only payload with specified length will be accepted.
|
||||
channel-type.config.bluetooth.service-number.uuid.label = Service UUID
|
||||
channel-type.config.bluetooth.service-number.uuid.description = Service UUID
|
||||
channel-type.config.bluetooth.service-raw.dataBegin.label = Data Begin
|
||||
channel-type.config.bluetooth.service-raw.dataBegin.description = Data begin index in the raw data byte array.
|
||||
channel-type.config.bluetooth.service-raw.dataLength.label = Data Length
|
||||
channel-type.config.bluetooth.service-raw.dataLength.description = Data length. If set to 0 the data will be loaded till the end.
|
||||
channel-type.config.bluetooth.service-raw.uuid.label = Service UUID
|
||||
channel-type.config.bluetooth.service-raw.uuid.description = Service UUID
|
||||
|
||||
# thing status descriptions
|
||||
|
||||
offline.communication-error = No data received for {0} minute
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?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="listener" extensible="manufacturer-number,manufacturer-raw,service-number,service-raw">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="roaming"/>
|
||||
<bridge-type-ref id="bluegiga"/>
|
||||
<bridge-type-ref id="bluez"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Listener</label>
|
||||
<description>Bluetooth Listener</description>
|
||||
|
||||
<channels>
|
||||
<channel id="rssi" typeId="rssi"/>
|
||||
<channel id="interval" typeId="advertise-interval"/>
|
||||
</channels>
|
||||
<properties>
|
||||
<property name="advertisingInterval">-</property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="address" type="text" required="true">
|
||||
<label>Address</label>
|
||||
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="changeByteOrder" type="boolean">
|
||||
<label>Big Endian</label>
|
||||
<description>Multibyte item byte order for number channels. For items with length 2, 4 or 8 bytes byte order could
|
||||
be changed. Default (false) order is little endian. For big endian order set this to true.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="autoChannelCreation" type="boolean">
|
||||
<label>Automatic Channel Creation</label>
|
||||
<description>Enable automatic channel creation from received service and manufacturer data</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="dataTimeout" min="1" type="integer">
|
||||
<label>Data Timeout</label>
|
||||
<description>Maximum time in minutes before a communication error is raised</description>
|
||||
<default>1</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="advertise-interval">
|
||||
<item-type>Number</item-type>
|
||||
<label>Advertising Interval</label>
|
||||
<description>Advertising interval</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="manufacturer-number">
|
||||
<item-type>Number</item-type>
|
||||
<label>Manufacturer Numeric Data</label>
|
||||
<description>Manufacturer numeric data</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description>
|
||||
<parameter name="dataBegin" type="integer" min="0">
|
||||
<label>Data Begin</label>
|
||||
<description>Data begin in the raw data byte array.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="dataLength" type="integer" min="1" pattern="^[1,2,4,8]$">
|
||||
<label>Data Length</label>
|
||||
<description>Data length. Only values 1, 2, 4, 8 are allowed (for datatypes byte, short, integer and long
|
||||
respectively).</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="multiplyer" type="decimal">
|
||||
<label>Multiplyer</label>
|
||||
<description>Simple data conversion by value multiplication.</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
<parameter name="payloadLength" type="decimal">
|
||||
<label>Payload Length</label>
|
||||
<description>Expected payload length. Default is 0 that means no payload length check is provided. Otherwise only
|
||||
payload with specified length will be accepted.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
<channel-type id="manufacturer-raw">
|
||||
<item-type>String</item-type>
|
||||
<label>Manufacturer Raw Data</label>
|
||||
<description>Manufacturer raw data</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description>
|
||||
<parameter name="dataBegin" type="integer" min="0">
|
||||
<label>Data begin</label>
|
||||
<description>Data begin in the raw data byte array.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="dataLength" type="integer" min="0">
|
||||
<label>Data Length</label>
|
||||
<description>Data length. If set to 0 the data will be loaded till the end.</description>
|
||||
<default>2</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
<channel-type id="service-number">
|
||||
<item-type>Number</item-type>
|
||||
<label>Service Numeric Data</label>
|
||||
<description>Service numeric data</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description>
|
||||
<parameter name="uuid" type="text" required="true">
|
||||
<label>Service UUID</label>
|
||||
<description>Service UUID</description>
|
||||
</parameter>
|
||||
<parameter name="dataBegin" type="integer" min="0">
|
||||
<label>Data Begin</label>
|
||||
<description>Data begin in the raw data byte array.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="dataLength" type="integer" min="1" pattern="^[1,2,4,8]$">
|
||||
<label>Data Length</label>
|
||||
<description>Data length. Only values 1, 2, 4, 8 are allowed (for datatypes byte, short, integer and long
|
||||
respectively).</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="multiplyer" type="decimal">
|
||||
<label>Multiplyer</label>
|
||||
<description>Simple data conversion by value multiplication.</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
<parameter name="payloadLength" type="decimal">
|
||||
<label>Payload Length</label>
|
||||
<description>Expected payload length. Default is 0 that means no payload length check is provided. Otherwise only
|
||||
payload with specified length will be accepted.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
<channel-type id="service-raw">
|
||||
<item-type>String</item-type>
|
||||
<label>Service Raw Data</label>
|
||||
<description>Service raw data</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description>
|
||||
<parameter name="uuid" type="text" required="true">
|
||||
<label>Service UUID</label>
|
||||
<description>Service UUID</description>
|
||||
</parameter>
|
||||
<parameter name="dataBegin" type="integer" min="0">
|
||||
<label>Data Begin</label>
|
||||
<description>Data begin index in the raw data byte array.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="dataLength" type="integer" min="0">
|
||||
<label>Data Length</label>
|
||||
<description>Data length. If set to 0 the data will be loaded till the end.</description>
|
||||
<default>2</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
@@ -87,6 +87,7 @@
|
||||
<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.listener</module>
|
||||
<module>org.openhab.binding.bluetooth.radoneye</module>
|
||||
<module>org.openhab.binding.bluetooth.roaming</module>
|
||||
<module>org.openhab.binding.bluetooth.ruuvitag</module>
|
||||
|
||||
Reference in New Issue
Block a user