[bluetooth.listener] Initial binding contribution (#18733)

* Initial bluetooth.listener binding contribution.

Signed-off-by: docbender <vita.tucek@seznam.cz>
This commit is contained in:
Vita
2025-07-13 09:44:24 +02:00
committed by GitHub
parent f317bb8806
commit 8c6db7519b
13 changed files with 904 additions and 0 deletions
+1
View File
@@ -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
+5
View File
@@ -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>
@@ -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";
}
@@ -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;
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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
@@ -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>
+1
View File
@@ -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>