mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[bluetooth.generic] Added support for generic bluetooth devices (#8775)
* Generic Bluetooth Binding Initial Contribution Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
parent
0c30d90757
commit
fb7fcd886d
@ -27,6 +27,7 @@
|
||||
/bundles/org.openhab.binding.bluetooth.bluez/ @cdjackson @kaikreuzer
|
||||
/bundles/org.openhab.binding.bluetooth.blukii/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.bluetooth.daikinmadoka/ @blafois
|
||||
/bundles/org.openhab.binding.bluetooth.generic/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
|
||||
/bundles/org.openhab.binding.boschindego/ @jofleck
|
||||
|
@ -126,6 +126,11 @@
|
||||
<artifactId>org.openhab.binding.bluetooth.daikinmadoka</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth.generic</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth.roaming</artifactId>
|
||||
|
20
bundles/org.openhab.binding.bluetooth.generic/NOTICE
Normal file
20
bundles/org.openhab.binding.bluetooth.generic/NOTICE
Normal file
@ -0,0 +1,20 @@
|
||||
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
|
||||
|
||||
== Third-party Content
|
||||
|
||||
Vlad Kolotov
|
||||
* License: Apache 2.0 License
|
||||
* Project: https://github.com/sputnikdev/bluetooth-gatt-parser
|
||||
* Source: https://github.com/sputnikdev/bluetooth-gatt-parser
|
33
bundles/org.openhab.binding.bluetooth.generic/README.md
Normal file
33
bundles/org.openhab.binding.bluetooth.generic/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Generic Bluetooth Device
|
||||
|
||||
This binding adds support for devices that expose [Bluetooth Generic Attributes (GATT)](https://www.bluetooth.com/specifications/gatt/)
|
||||
|
||||
## Supported Things
|
||||
|
||||
Only a single thing type is added by this binding:
|
||||
|
||||
| Thing Type ID | Description |
|
||||
|---------------|-------------------------------------------------|
|
||||
| generic | A generic connectable bluetooth device |
|
||||
|
||||
## Discovery
|
||||
|
||||
As any other Bluetooth device, generic bluetooth devices are discovered automatically by the corresponding bridge.
|
||||
Generic bluetooth devices will be discovered for any connectable bluetooth device that doesn't match another bluetooth binding.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------------|----------|---------|---------------------------------------------------------------------|
|
||||
| address | yes | | The address of the bluetooth device (in format "XX:XX:XX:XX:XX:XX") |
|
||||
| pollingInterval | no | 30 | The frequency at which readable characteristics will refresh |
|
||||
|
||||
## Channels
|
||||
|
||||
Channels will be dynamically created based on types of characteristics the device supports.
|
||||
This binding contains a mostly complete database of standardized GATT services and characteristics
|
||||
that is used to map characteristics to one or multiple channels.
|
||||
|
||||
Characteristics not in the database will be mapped to a single `String` channel labeled `Unknown`.
|
||||
The data visible from unknown channels will be the raw binary data formated as hexadecimal.
|
||||
Data written (if the unknown characteristic has write support) to unknown channels must likewise be in hexadecimal.
|
44
bundles/org.openhab.binding.bluetooth.generic/pom.xml
Normal file
44
bundles/org.openhab.binding.bluetooth.generic/pom.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.bluetooth.generic</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Generic Bluetooth Adapter</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.sputnikdev</groupId>
|
||||
<artifactId>bluetooth-gatt-parser</artifactId>
|
||||
<version>1.9.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-collections</groupId>
|
||||
<artifactId>commons-collections</artifactId>
|
||||
<version>3.2.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
<version>1.9.3</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<features name="org.openhab.binding.bluetooth.generic-${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-generic" description="Bluetooth Binding Generic" 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.generic/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.sputnikdev.bluetooth.gattparser.BluetoothGattParser;
|
||||
import org.sputnikdev.bluetooth.gattparser.FieldHolder;
|
||||
import org.sputnikdev.bluetooth.gattparser.GattRequest;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.Enumeration;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.Field;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.FieldFormat;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.FieldType;
|
||||
|
||||
/**
|
||||
* The {@link BluetoothChannelUtils} contains utility functions used by the GattChannelHandler
|
||||
*
|
||||
* @author Vlad Kolotov - Original author
|
||||
* @author Connor Petty - Modified for openHAB use
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BluetoothChannelUtils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BluetoothChannelUtils.class);
|
||||
|
||||
public static String encodeFieldID(Field field) {
|
||||
String requirements = Optional.ofNullable(field.getRequirements()).orElse(Collections.emptyList()).stream()
|
||||
.collect(Collectors.joining());
|
||||
return encodeFieldName(field.getName() + requirements);
|
||||
}
|
||||
|
||||
public static String encodeFieldName(String fieldName) {
|
||||
return Base64.getEncoder().encodeToString(fieldName.getBytes(StandardCharsets.UTF_8)).replace("=", "");
|
||||
}
|
||||
|
||||
public static String decodeFieldName(String encodedFieldName) {
|
||||
return new String(Base64.getDecoder().decode(encodedFieldName), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static @Nullable String getItemType(Field field) {
|
||||
FieldFormat format = field.getFormat();
|
||||
if (format == null) {
|
||||
// unknown format
|
||||
return null;
|
||||
}
|
||||
switch (field.getFormat().getType()) {
|
||||
case BOOLEAN:
|
||||
return "Switch";
|
||||
case UINT:
|
||||
case SINT:
|
||||
case FLOAT_IEE754:
|
||||
case FLOAT_IEE11073:
|
||||
BluetoothUnit unit = BluetoothUnit.findByType(field.getUnit());
|
||||
if (unit != null) {
|
||||
// TODO
|
||||
// return "Number:" + unit.getUnit().getDimension();
|
||||
}
|
||||
return "Number";
|
||||
case UTF8S:
|
||||
case UTF16S:
|
||||
return "String";
|
||||
case STRUCT:
|
||||
return "String";
|
||||
// unsupported format
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static State convert(BluetoothGattParser parser, FieldHolder holder) {
|
||||
State state;
|
||||
if (holder.isValueSet()) {
|
||||
if (holder.getField().getFormat().isBoolean()) {
|
||||
state = OnOffType.from(Boolean.TRUE.equals(holder.getBoolean()));
|
||||
} else {
|
||||
// check if we can use enumerations
|
||||
if (holder.getField().hasEnumerations()) {
|
||||
Enumeration enumeration = holder.getEnumeration();
|
||||
if (enumeration != null) {
|
||||
if (holder.getField().getFormat().isNumber()) {
|
||||
return new DecimalType(new BigDecimal(enumeration.getKey()));
|
||||
} else {
|
||||
return new StringType(enumeration.getKey().toString());
|
||||
}
|
||||
}
|
||||
// fall back to simple types
|
||||
}
|
||||
if (holder.getField().getFormat().isNumber()) {
|
||||
state = new DecimalType(holder.getBigDecimal());
|
||||
} else if (holder.getField().getFormat().isStruct()) {
|
||||
state = new StringType(parser.parse(holder.getBytes(), 16));
|
||||
} else {
|
||||
state = new StringType(holder.getString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state = UnDefType.UNDEF;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
public static void updateHolder(BluetoothGattParser parser, GattRequest request, String fieldName, State state) {
|
||||
Field field = request.getFieldHolder(fieldName).getField();
|
||||
FieldType fieldType = field.getFormat().getType();
|
||||
if (fieldType == FieldType.BOOLEAN) {
|
||||
OnOffType onOffType = convert(state, OnOffType.class);
|
||||
if (onOffType == null) {
|
||||
logger.debug("Could not convert state to OnOffType: {} : {} : {} ", request.getCharacteristicUUID(),
|
||||
fieldName, state);
|
||||
return;
|
||||
}
|
||||
request.setField(fieldName, onOffType == OnOffType.ON);
|
||||
return;
|
||||
}
|
||||
if (field.hasEnumerations()) {
|
||||
// check if we can use enumerations
|
||||
Enumeration enumeration = getEnumeration(field, state);
|
||||
if (enumeration != null) {
|
||||
request.setField(fieldName, enumeration);
|
||||
return;
|
||||
} else {
|
||||
logger.debug("Could not convert state to enumeration: {} : {} : {} ", request.getCharacteristicUUID(),
|
||||
fieldName, state);
|
||||
}
|
||||
// fall back to simple types
|
||||
}
|
||||
switch (fieldType) {
|
||||
case UINT:
|
||||
case SINT: {
|
||||
DecimalType decimalType = convert(state, DecimalType.class);
|
||||
if (decimalType == null) {
|
||||
logger.debug("Could not convert state to DecimalType: {} : {} : {} ",
|
||||
request.getCharacteristicUUID(), fieldName, state);
|
||||
return;
|
||||
}
|
||||
request.setField(fieldName, decimalType.longValue());
|
||||
return;
|
||||
}
|
||||
case FLOAT_IEE754:
|
||||
case FLOAT_IEE11073: {
|
||||
DecimalType decimalType = convert(state, DecimalType.class);
|
||||
if (decimalType == null) {
|
||||
logger.debug("Could not convert state to DecimalType: {} : {} : {} ",
|
||||
request.getCharacteristicUUID(), fieldName, state);
|
||||
return;
|
||||
}
|
||||
request.setField(fieldName, decimalType.doubleValue());
|
||||
return;
|
||||
}
|
||||
case UTF8S:
|
||||
case UTF16S: {
|
||||
StringType textType = convert(state, StringType.class);
|
||||
if (textType == null) {
|
||||
logger.debug("Could not convert state to StringType: {} : {} : {} ",
|
||||
request.getCharacteristicUUID(), fieldName, state);
|
||||
return;
|
||||
}
|
||||
request.setField(fieldName, textType.toString());
|
||||
return;
|
||||
}
|
||||
case STRUCT:
|
||||
StringType textType = convert(state, StringType.class);
|
||||
if (textType == null) {
|
||||
logger.debug("Could not convert state to StringType: {} : {} : {} ",
|
||||
request.getCharacteristicUUID(), fieldName, state);
|
||||
return;
|
||||
}
|
||||
String text = textType.toString().trim();
|
||||
if (text.startsWith("[")) {
|
||||
request.setField(fieldName, parser.serialize(text, 16));
|
||||
} else {
|
||||
request.setField(fieldName, new BigInteger(text));
|
||||
}
|
||||
return;
|
||||
// unsupported format
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Enumeration getEnumeration(Field field, State state) {
|
||||
DecimalType decimalType = convert(state, DecimalType.class);
|
||||
if (decimalType != null) {
|
||||
try {
|
||||
return field.getEnumeration(new BigInteger(decimalType.toString()));
|
||||
} catch (NumberFormatException ex) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static <T extends State> @Nullable T convert(State state, Class<T> typeClass) {
|
||||
return state.as(typeClass);
|
||||
}
|
||||
}
|
@ -0,0 +1,363 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Angle;
|
||||
import javax.measure.quantity.Area;
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.ElectricCharge;
|
||||
import javax.measure.quantity.Frequency;
|
||||
import javax.measure.quantity.Length;
|
||||
import javax.measure.quantity.Mass;
|
||||
import javax.measure.quantity.Speed;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.core.library.dimension.ArealDensity;
|
||||
import org.openhab.core.library.dimension.VolumetricFlowRate;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.MetricPrefix;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
|
||||
import tec.uom.se.format.SimpleUnitFormat;
|
||||
import tec.uom.se.function.MultiplyConverter;
|
||||
import tec.uom.se.function.PiMultiplierConverter;
|
||||
import tec.uom.se.function.RationalConverter;
|
||||
import tec.uom.se.unit.ProductUnit;
|
||||
import tec.uom.se.unit.TransformedUnit;
|
||||
import tec.uom.se.unit.Units;
|
||||
|
||||
/**
|
||||
* The {@link BluetoothUnit} maps bluetooth units to openHAB units.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum BluetoothUnit {
|
||||
|
||||
UNITLESS(0x2700, "org.bluetooth.unit.unitless", SmartHomeUnits.ONE),
|
||||
METRE(0x2701, "org.bluetooth.unit.length.metre", SIUnits.METRE),
|
||||
KILOGRAM(0x2702, "org.bluetooth.unit.mass.kilogram", SIUnits.KILOGRAM),
|
||||
SECOND(0x2703, "org.bluetooth.unit.time.second", SmartHomeUnits.SECOND),
|
||||
AMPERE(0x2704, "org.bluetooth.unit.electric_current.ampere", SmartHomeUnits.AMPERE),
|
||||
KELVIN(0x2705, "org.bluetooth.unit.thermodynamic_temperature.kelvin", SmartHomeUnits.KELVIN),
|
||||
MOLE(0x2706, "org.bluetooth.unit.amount_of_substance.mole", SmartHomeUnits.MOLE),
|
||||
CANDELA(0x2707, "org.bluetooth.unit.luminous_intensity.candela", SmartHomeUnits.CANDELA),
|
||||
SQUARE_METRES(0x2710, "org.bluetooth.unit.area.square_metres", SIUnits.SQUARE_METRE),
|
||||
CUBIC_METRES(0x2711, "org.bluetooth.unit.volume.cubic_metres", SIUnits.CUBIC_METRE),
|
||||
METRE_PER_SECOND(0x2712, "org.bluetooth.unit.velocity.metres_per_second", SmartHomeUnits.METRE_PER_SECOND),
|
||||
METRE_PER_SQUARE_SECOND(0X2713, "org.bluetooth.unit.acceleration.metres_per_second_squared",
|
||||
SmartHomeUnits.METRE_PER_SQUARE_SECOND),
|
||||
WAVENUMBER(0x2714, "org.bluetooth.unit.wavenumber.reciprocal_metre", SmartHomeUnits.ONE),
|
||||
KILOGRAM_PER_CUBIC_METRE(0x2715, "org.bluetooth.unit.density.kilogram_per_cubic_metre",
|
||||
SmartHomeUnits.KILOGRAM_PER_CUBICMETRE),
|
||||
KILOGRAM_PER_SQUARE_METRE(0x2716, "org.bluetooth.unit.surface_density.kilogram_per_square_metre",
|
||||
BUnits.KILOGRAM_PER_SQUARE_METER),
|
||||
CUBIC_METRE_PER_KILOGRAM(0x2717, "org.bluetooth.unit.specific_volume.cubic_metre_per_kilogram", SmartHomeUnits.ONE),
|
||||
AMPERE_PER_SQUARE_METRE(0x2718, "org.bluetooth.unit.current_density.ampere_per_square_metre", SmartHomeUnits.ONE),
|
||||
AMPERE_PER_METRE(0x2719, "org.bluetooth.unit.magnetic_field_strength.ampere_per_metre", SmartHomeUnits.ONE),
|
||||
MOLE_PER_CUBIC_METRE(0x271A, "org.bluetooth.unit.amount_concentration.mole_per_cubic_metre", SmartHomeUnits.ONE),
|
||||
CONCENTRATION_KILOGRAM_PER_CUBIC_METRE(0x271B, "org.bluetooth.unit.mass_concentration.kilogram_per_cubic_metre",
|
||||
SmartHomeUnits.KILOGRAM_PER_CUBICMETRE),
|
||||
CANDELA_PER_SQUARE_METRE(0x271C, "org.bluetooth.unit.luminance.candela_per_square_metre", SmartHomeUnits.ONE),
|
||||
REFRACTIVE_INDEX(0x271D, "org.bluetooth.unit.refractive_index", SmartHomeUnits.ONE),
|
||||
RELATIVE_PERMEABILITY(0x271E, "org.bluetooth.unit.relative_permeability", SmartHomeUnits.ONE),
|
||||
RADIAN(0x2720, "org.bluetooth.unit.plane_angle.radian", SmartHomeUnits.RADIAN),
|
||||
STERADIAN(0x2721, "org.bluetooth.unit.solid_angle.steradian", SmartHomeUnits.STERADIAN),
|
||||
HERTZ(0x2722, "org.bluetooth.unit.frequency.hertz", SmartHomeUnits.HERTZ),
|
||||
NEWTON(0x2723, "org.bluetooth.unit.force.newton", SmartHomeUnits.NEWTON),
|
||||
PASCAL(0x2724, "org.bluetooth.unit.pressure.pascal", SIUnits.PASCAL),
|
||||
JOULE(0x2725, "org.bluetooth.unit.energy.joule", SmartHomeUnits.JOULE),
|
||||
WATT(0x2726, "org.bluetooth.unit.power.watt", SmartHomeUnits.WATT),
|
||||
COULOMB(0x2727, "org.bluetooth.unit.electric_charge.coulomb", SmartHomeUnits.COULOMB),
|
||||
VOLT(0x2728, "org.bluetooth.unit.electric_potential_difference.volt", SmartHomeUnits.VOLT),
|
||||
FARAD(0x2729, "org.bluetooth.unit.capacitance.farad", SmartHomeUnits.FARAD),
|
||||
OHM(0x272A, "org.bluetooth.unit.electric_resistance.ohm", SmartHomeUnits.OHM),
|
||||
SIEMENS(0x272B, "org.bluetooth.unit.electric_conductance.siemens", SmartHomeUnits.SIEMENS),
|
||||
WEBER(0x272C, "org.bluetooth.unit.magnetic_flux.weber", SmartHomeUnits.WEBER),
|
||||
TESLA(0x272D, "org.bluetooth.unit.magnetic_flux_density.tesla", SmartHomeUnits.TESLA),
|
||||
HENRY(0x272E, "org.bluetooth.unit.inductance.henry", SmartHomeUnits.HENRY),
|
||||
DEGREE_CELSIUS(0x272F, "org.bluetooth.unit.thermodynamic_temperature.degree_celsius", SIUnits.CELSIUS),
|
||||
LUMEN(0x2730, "org.bluetooth.unit.luminous_flux.lumen", SmartHomeUnits.LUMEN),
|
||||
LUX(0x2731, "org.bluetooth.unit.illuminance.lux", SmartHomeUnits.LUX),
|
||||
BECQUEREL(0x2732, "org.bluetooth.unit.activity_referred_to_a_radionuclide.becquerel", SmartHomeUnits.BECQUEREL),
|
||||
GRAY(0x2733, "org.bluetooth.unit.absorbed_dose.gray", SmartHomeUnits.GRAY),
|
||||
SIEVERT(0x2734, "org.bluetooth.unit.dose_equivalent.sievert", SmartHomeUnits.SIEVERT),
|
||||
KATAL(0x2735, "org.bluetooth.unit.catalytic_activity.katal", SmartHomeUnits.KATAL),
|
||||
PASCAL_SECOND(0x2740, "org.bluetooth.unit.dynamic_viscosity.pascal_second", SmartHomeUnits.ONE),
|
||||
NEWTON_METRE(0x2741, "org.bluetooth.unit.moment_of_force.newton_metre", SmartHomeUnits.ONE),
|
||||
NEWTON_PER_METRE(0x2742, "org.bluetooth.unit.surface_tension.newton_per_metre", SmartHomeUnits.ONE),
|
||||
RADIAN_PER_SECOND(0x2743, "org.bluetooth.unit.angular_velocity.radian_per_second", SmartHomeUnits.ONE),
|
||||
RADIAN_PER_SECOND_SQUARED(0x2744, "org.bluetooth.unit.angular_acceleration.radian_per_second_squared",
|
||||
SmartHomeUnits.ONE),
|
||||
FLUX_WATT_PER_SQUARE_METRE(0x2745, "org.bluetooth.unit.heat_flux_density.watt_per_square_metre",
|
||||
SmartHomeUnits.ONE),
|
||||
JOULE_PER_KELVIN(0x2746, "org.bluetooth.unit.heat_capacity.joule_per_kelvin", SmartHomeUnits.ONE),
|
||||
JOULE_PER_KILOGRAM_KELVIN(0x2747, "org.bluetooth.unit.specific_heat_capacity.joule_per_kilogram_kelvin",
|
||||
SmartHomeUnits.ONE),
|
||||
JOULE_PER_KILOGRAM(0x2748, "org.bluetooth.unit.specific_energy.joule_per_kilogram", SmartHomeUnits.ONE),
|
||||
WATT_PER_METRE_KELVIN(0x2749, "org.bluetooth.unit.thermal_conductivity.watt_per_metre_kelvin", SmartHomeUnits.ONE),
|
||||
JOULE_PER_CUBIC_METRE(0x274A, "org.bluetooth.unit.energy_density.joule_per_cubic_metre", SmartHomeUnits.ONE),
|
||||
VOLT_PER_METRE(0x274B, "org.bluetooth.unit.electric_field_strength.volt_per_metre", SmartHomeUnits.ONE),
|
||||
CHARGE_DENSITY_COULOMB_PER_CUBIC_METRE(0x274C, "org.bluetooth.unit.electric_charge_density.coulomb_per_cubic_metre",
|
||||
SmartHomeUnits.ONE),
|
||||
CHARGE_DENSITY_COULOMB_PER_SQUARE_METRE(0x274D,
|
||||
"org.bluetooth.unit.surface_charge_density.coulomb_per_square_metre", SmartHomeUnits.ONE),
|
||||
FLUX_DENSITY_COULOMB_PER_SQUARE_METRE(0x274E, "org.bluetooth.unit.electric_flux_density.coulomb_per_square_metre",
|
||||
SmartHomeUnits.ONE),
|
||||
FARAD_PER_METRE(0x274F, "org.bluetooth.unit.permittivity.farad_per_metre", SmartHomeUnits.ONE),
|
||||
HENRY_PER_METRE(0x2750, "org.bluetooth.unit.permeability.henry_per_metre", SmartHomeUnits.ONE),
|
||||
JOULE_PER_MOLE(0x2751, "org.bluetooth.unit.molar_energy.joule_per_mole", SmartHomeUnits.ONE),
|
||||
JOULE_PER_MOLE_KELVIN(0x2752, "org.bluetooth.unit.molar_entropy.joule_per_mole_kelvin", SmartHomeUnits.ONE),
|
||||
COULOMB_PER_KILOGRAM(0x2753, "org.bluetooth.unit.exposure.coulomb_per_kilogram", SmartHomeUnits.ONE),
|
||||
GRAY_PER_SECOND(0x2754, "org.bluetooth.unit.absorbed_dose_rate.gray_per_second", BUnits.GRAY_PER_SECOND),
|
||||
WATT_PER_STERADIAN(0x2755, "org.bluetooth.unit.radiant_intensity.watt_per_steradian", BUnits.WATT_PER_STERADIAN),
|
||||
WATT_PER_STERADIAN_PER_SQUARE_METRE(0x2756, "org.bluetooth.unit.radiance.watt_per_square_metre_steradian",
|
||||
BUnits.WATT_PER_STERADIAN_PER_SQUARE_METRE),
|
||||
KATAL_PER_CUBIC_METRE(0x2757, "org.bluetooth.unit.catalytic_activity_concentration.katal_per_cubic_metre",
|
||||
SmartHomeUnits.ONE),
|
||||
MINUTE(0x2760, "org.bluetooth.unit.time.minute", SmartHomeUnits.MINUTE),
|
||||
HOUR(0x2761, "org.bluetooth.unit.time.hour", SmartHomeUnits.HOUR),
|
||||
DAY(0x2762, "org.bluetooth.unit.time.day", SmartHomeUnits.DAY),
|
||||
ANGLE_DEGREE(0x2763, "org.bluetooth.unit.plane_angle.degree", SmartHomeUnits.DEGREE_ANGLE),
|
||||
ANGLE_MINUTE(0x2764, "org.bluetooth.unit.plane_angle.minute", BUnits.MINUTE_ANGLE),
|
||||
ANGLE_SECOND(0x2765, "org.bluetooth.unit.plane_angle.second", BUnits.SECOND_ANGLE),
|
||||
HECTARE(0x2766, "org.bluetooth.unit.area.hectare", BUnits.HECTARE),
|
||||
LITRE(0x2767, "org.bluetooth.unit.volume.litre", SmartHomeUnits.LITRE),
|
||||
TONNE(0x2768, "org.bluetooth.unit.mass.tonne", MetricPrefix.KILO(SIUnits.KILOGRAM)),
|
||||
BAR(0x2780, "org.bluetooth.unit.pressure.bar", SmartHomeUnits.BAR),
|
||||
MILLIMETRE_OF_MERCURY(0x2781, "org.bluetooth.unit.pressure.millimetre_of_mercury",
|
||||
SmartHomeUnits.MILLIMETRE_OF_MERCURY),
|
||||
ÅNGSTRÖM(0x2782, "org.bluetooth.unit.length.ångström", SmartHomeUnits.ONE),
|
||||
NAUTICAL_MILE(0x2783, "org.bluetooth.unit.length.nautical_mile", BUnits.NAUTICAL_MILE),
|
||||
BARN(0x2784, "org.bluetooth.unit.area.barn", BUnits.BARN),
|
||||
KNOT(0x2785, "org.bluetooth.unit.velocity.knot", SmartHomeUnits.KNOT),
|
||||
NEPER(0x2786, "org.bluetooth.unit.logarithmic_radio_quantity.neper", SmartHomeUnits.ONE),
|
||||
BEL(0x2787, "org.bluetooth.unit.logarithmic_radio_quantity.bel", SmartHomeUnits.ONE),
|
||||
YARD(0x27A0, "org.bluetooth.unit.length.yard", ImperialUnits.YARD),
|
||||
PARSEC(0x27A1, "org.bluetooth.unit.length.parsec", SmartHomeUnits.ONE),
|
||||
INCH(0x27A2, "org.bluetooth.unit.length.inch", ImperialUnits.INCH),
|
||||
FOOT(0x27A3, "org.bluetooth.unit.length.foot", ImperialUnits.FOOT),
|
||||
MILE(0x27A4, "org.bluetooth.unit.length.mile", ImperialUnits.MILE),
|
||||
POUND_FORCE_PER_SQUARE_INCH(0x27A5, "org.bluetooth.unit.pressure.pound_force_per_square_inch", SmartHomeUnits.ONE),
|
||||
KILOMETRE_PER_HOUR(0x27A6, "org.bluetooth.unit.velocity.kilometre_per_hour", SIUnits.KILOMETRE_PER_HOUR),
|
||||
MILES_PER_HOUR(0x27A7, "org.bluetooth.unit.velocity.mile_per_hour", ImperialUnits.MILES_PER_HOUR),
|
||||
REVOLUTION_PER_MINUTE(0x27A8, "org.bluetooth.unit.angular_velocity.revolution_per_minute",
|
||||
BUnits.REVOLUTION_PER_MINUTE),
|
||||
GRAM_CALORIE(0x27A9, "org.bluetooth.unit.energy.gram_calorie", SmartHomeUnits.ONE),
|
||||
KILOGRAM_CALORIE(0x27AA, "org.bluetooth.unit.energy.kilogram_calorie", SmartHomeUnits.ONE),
|
||||
KILOWATT_HOUR(0x27AB, "org.bluetooth.unit.energy.kilowatt_hour", SmartHomeUnits.KILOWATT_HOUR),
|
||||
DEGREE_FAHRENHEIT(0x27AC, "org.bluetooth.unit.thermodynamic_temperature.degree_fahrenheit",
|
||||
ImperialUnits.FAHRENHEIT),
|
||||
PERCENTAGE(0x27AD, "org.bluetooth.unit.percentage", SmartHomeUnits.PERCENT),
|
||||
PER_MILLE(0x27AE, "org.bluetooth.unit.per_mille", SmartHomeUnits.ONE),
|
||||
BEATS_PER_MINUTE(0x27AF, "org.bluetooth.unit.period.beats_per_minute", BUnits.BEATS_PER_MINUTE),
|
||||
AMPERE_HOURS(0x27B0, "org.bluetooth.unit.electric_charge.ampere_hours", BUnits.AMPERE_HOUR),
|
||||
MILLIGRAM_PER_DECILITRE(0x27B1, "org.bluetooth.unit.mass_density.milligram_per_decilitre", SmartHomeUnits.ONE),
|
||||
MILLIMOLE_PER_LITRE(0x27B2, "org.bluetooth.unit.mass_density.millimole_per_litre", SmartHomeUnits.ONE),
|
||||
YEAR(0x27B3, "org.bluetooth.unit.time.year", SmartHomeUnits.YEAR),
|
||||
MONTH(0x27B4, "org.bluetooth.unit.time.month", SmartHomeUnits.ONE),
|
||||
COUNT_PER_CUBIC_METRE(0x27B5, "org.bluetooth.unit.concentration.count_per_cubic_metre", SmartHomeUnits.ONE),
|
||||
WATT_PER_SQUARE_METRE(0x27B6, "org.bluetooth.unit.irradiance.watt_per_square_metre", SmartHomeUnits.IRRADIANCE),
|
||||
MILLILITER_PER_KILOGRAM_PER_MINUTE(0x27B7, "org.bluetooth.unit.transfer_rate.milliliter_per_kilogram_per_minute",
|
||||
SmartHomeUnits.ONE),
|
||||
POUND(0x27B8, "org.bluetooth.unit.mass.pound", BUnits.POUND),
|
||||
METABOLIC_EQUIVALENT(0x27B9, "org.bluetooth.unit.metabolic_equivalent", SmartHomeUnits.ONE),
|
||||
STEP_PER_MINUTE(0x27BA, "org.bluetooth.unit.step_per_minute", BUnits.STEP_PER_MINUTE),
|
||||
STROKE_PER_MINUTE(0x27BC, "org.bluetooth.unit.stroke_per_minute", BUnits.STROKE_PER_MINUTE),
|
||||
KILOMETER_PER_MINUTE(0x27BD, "org.bluetooth.unit.velocity.kilometer_per_minute", BUnits.KILOMETRE_PER_MINUTE),
|
||||
LUMEN_PER_WATT(0x27BE, "org.bluetooth.unit.luminous_efficacy.lumen_per_watt", BUnits.LUMEN_PER_WATT),
|
||||
LUMEN_HOUR(0x27BF, "org.bluetooth.unit.luminous_energy.lumen_hour", BUnits.LUMEN_HOUR),
|
||||
LUX_HOUR(0x27C0, "org.bluetooth.unit.luminous_exposure.lux_hour", BUnits.LUX_HOUR),
|
||||
GRAM_PER_SECOND(0x27C1, "org.bluetooth.unit.mass_flow.gram_per_second", BUnits.GRAM_PER_SECOND),
|
||||
LITRE_PER_SECOND(0x27C2, "org.bluetooth.unit.volume_flow.litre_per_second", BUnits.LITRE_PER_SECOND),
|
||||
DECIBEL_SPL(0x27C3, "org.bluetooth.unit.sound_pressure.decibel_spl", SmartHomeUnits.ONE),
|
||||
PARTS_PER_MILLION(0x27C4, "org.bluetooth.unit.concentration.parts_per_million", SmartHomeUnits.PARTS_PER_MILLION),
|
||||
PARTS_PER_BILLION(0x27C5, "org.bluetooth.unit.concentration.parts_per_billion", SmartHomeUnits.PARTS_PER_BILLION);
|
||||
|
||||
private UUID uuid;
|
||||
|
||||
private String type;
|
||||
|
||||
private Unit<?> unit;
|
||||
|
||||
private BluetoothUnit(long key, String type, Unit<?> unit) {
|
||||
this.uuid = new UUID((key << 32) | 0x1000, BluetoothBindingConstants.BLUETOOTH_BASE_UUID);
|
||||
this.type = type;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public static @Nullable BluetoothUnit findByType(String type) {
|
||||
for (BluetoothUnit unit : BluetoothUnit.values()) {
|
||||
if (unit.type.equals(type)) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Unit<?> getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class contains the set of units that are not yet defined in SmarthomeUnits.
|
||||
* Once these units are added to the core then this class will be removed.
|
||||
*
|
||||
* @author cpetty
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public static class BUnits {
|
||||
public static final Unit<ArealDensity> KILOGRAM_PER_SQUARE_METER = addUnit(
|
||||
new ProductUnit<ArealDensity>(Units.KILOGRAM.divide(Units.SQUARE_METRE)));
|
||||
|
||||
public static final Unit<RadiationExposure> COULOMB_PER_KILOGRAM = addUnit(
|
||||
new ProductUnit<RadiationExposure>(Units.COULOMB.divide(Units.KILOGRAM)));
|
||||
|
||||
public static final Unit<RadiationDoseAbsorptionRate> GRAY_PER_SECOND = addUnit(
|
||||
new ProductUnit<RadiationDoseAbsorptionRate>(Units.GRAY.divide(Units.SECOND)));
|
||||
|
||||
public static final Unit<Mass> POUND = addUnit(
|
||||
new TransformedUnit<Mass>(Units.KILOGRAM, new MultiplyConverter(0.45359237)));
|
||||
|
||||
public static final Unit<Angle> MINUTE_ANGLE = addUnit(new TransformedUnit<Angle>(Units.RADIAN,
|
||||
new PiMultiplierConverter().concatenate(new RationalConverter(1, 180 * 60))));
|
||||
|
||||
public static final Unit<Angle> SECOND_ANGLE = addUnit(new TransformedUnit<Angle>(Units.RADIAN,
|
||||
new PiMultiplierConverter().concatenate(new RationalConverter(1, 180 * 60 * 60))));
|
||||
|
||||
public static final Unit<Area> HECTARE = addUnit(Units.SQUARE_METRE.multiply(10000.0));
|
||||
public static final Unit<Area> BARN = addUnit(Units.SQUARE_METRE.multiply(10E-28));
|
||||
|
||||
public static final Unit<Length> NAUTICAL_MILE = addUnit(SIUnits.METRE.multiply(1852.0));
|
||||
|
||||
public static final Unit<RadiantIntensity> WATT_PER_STERADIAN = addUnit(
|
||||
new ProductUnit<RadiantIntensity>(Units.WATT.divide(Units.STERADIAN)));
|
||||
|
||||
public static final Unit<Radiance> WATT_PER_STERADIAN_PER_SQUARE_METRE = addUnit(
|
||||
new ProductUnit<Radiance>(WATT_PER_STERADIAN.divide(Units.SQUARE_METRE)));
|
||||
|
||||
public static final Unit<Frequency> CYCLES_PER_MINUTE = addUnit(new TransformedUnit<Frequency>(Units.HERTZ,
|
||||
new RationalConverter(BigInteger.valueOf(60), BigInteger.ONE)));
|
||||
|
||||
public static final Unit<Angle> REVOLUTION = addUnit(new TransformedUnit<Angle>(Units.RADIAN,
|
||||
new PiMultiplierConverter().concatenate(new RationalConverter(2, 1))));
|
||||
public static final Unit<AngularVelocity> REVOLUTION_PER_MINUTE = addUnit(
|
||||
new ProductUnit<AngularVelocity>(REVOLUTION.divide(Units.MINUTE)));
|
||||
|
||||
public static final Unit<Dimensionless> STEPS = addUnit(SmartHomeUnits.ONE.alternate("steps"));
|
||||
public static final Unit<Dimensionless> BEATS = addUnit(SmartHomeUnits.ONE.alternate("beats"));
|
||||
public static final Unit<Dimensionless> STROKE = addUnit(SmartHomeUnits.ONE.alternate("stroke"));
|
||||
|
||||
public static final Unit<Frequency> STEP_PER_MINUTE = addUnit(
|
||||
new ProductUnit<Frequency>(STEPS.divide(Units.MINUTE)));
|
||||
|
||||
public static final Unit<Frequency> BEATS_PER_MINUTE = addUnit(
|
||||
new ProductUnit<Frequency>(BEATS.divide(Units.MINUTE)));
|
||||
|
||||
public static final Unit<Frequency> STROKE_PER_MINUTE = addUnit(
|
||||
new ProductUnit<Frequency>(STROKE.divide(Units.MINUTE)));
|
||||
|
||||
public static final Unit<MassFlowRate> GRAM_PER_SECOND = addUnit(
|
||||
new ProductUnit<MassFlowRate>(Units.GRAM.divide(Units.SECOND)));
|
||||
|
||||
public static final Unit<LuminousEfficacy> LUMEN_PER_WATT = addUnit(
|
||||
new ProductUnit<LuminousEfficacy>(Units.LUMEN.divide(Units.WATT)));
|
||||
|
||||
public static final Unit<LuminousEnergy> LUMEN_SECOND = addUnit(
|
||||
new ProductUnit<LuminousEnergy>(Units.LUMEN.multiply(Units.SECOND)));
|
||||
|
||||
public static final Unit<LuminousEnergy> LUMEN_HOUR = addUnit(
|
||||
new ProductUnit<LuminousEnergy>(Units.LUMEN.multiply(Units.HOUR)));
|
||||
|
||||
public static final Unit<ElectricCharge> AMPERE_HOUR = addUnit(
|
||||
new ProductUnit<ElectricCharge>(Units.AMPERE.multiply(Units.HOUR)));
|
||||
|
||||
public static final Unit<LuminousExposure> LUX_HOUR = addUnit(
|
||||
new ProductUnit<LuminousExposure>(Units.LUX.multiply(Units.HOUR)));
|
||||
|
||||
public static final Unit<Speed> KILOMETRE_PER_MINUTE = addUnit(Units.KILOMETRE_PER_HOUR.multiply(60.0));
|
||||
|
||||
public static final Unit<VolumetricFlowRate> LITRE_PER_SECOND = addUnit(
|
||||
new ProductUnit<VolumetricFlowRate>(Units.LITRE.divide(Units.SECOND)));
|
||||
|
||||
static {
|
||||
SimpleUnitFormat.getInstance().label(GRAY_PER_SECOND, "Gy/s");
|
||||
SimpleUnitFormat.getInstance().label(MINUTE_ANGLE, "'");
|
||||
SimpleUnitFormat.getInstance().label(SECOND_ANGLE, "\"");
|
||||
SimpleUnitFormat.getInstance().label(HECTARE, "ha");
|
||||
SimpleUnitFormat.getInstance().label(NAUTICAL_MILE, "NM");
|
||||
SimpleUnitFormat.getInstance().label(KILOGRAM_PER_SQUARE_METER, "kg/m²");
|
||||
SimpleUnitFormat.getInstance().label(POUND, "lb");
|
||||
SimpleUnitFormat.getInstance().label(CYCLES_PER_MINUTE, "cpm");
|
||||
SimpleUnitFormat.getInstance().label(GRAM_PER_SECOND, "g/s");
|
||||
SimpleUnitFormat.getInstance().label(LUMEN_SECOND, "lm·s");
|
||||
SimpleUnitFormat.getInstance().label(LUMEN_HOUR, "lm·h");
|
||||
SimpleUnitFormat.getInstance().label(LUMEN_PER_WATT, "lm/W");
|
||||
SimpleUnitFormat.getInstance().label(LUX_HOUR, "lx·h");
|
||||
SimpleUnitFormat.getInstance().label(KILOMETRE_PER_MINUTE, "km/min");
|
||||
SimpleUnitFormat.getInstance().label(LITRE_PER_SECOND, "l/s");
|
||||
SimpleUnitFormat.getInstance().label(BEATS_PER_MINUTE, "bpm");
|
||||
SimpleUnitFormat.getInstance().label(STEP_PER_MINUTE, "steps/min");
|
||||
SimpleUnitFormat.getInstance().label(STROKE_PER_MINUTE, "spm");
|
||||
SimpleUnitFormat.getInstance().label(REVOLUTION_PER_MINUTE, "rpm");
|
||||
}
|
||||
|
||||
private static <U extends Unit<?>> U addUnit(U unit) {
|
||||
return unit;
|
||||
}
|
||||
|
||||
public interface AngularVelocity extends Quantity<AngularVelocity> {
|
||||
}
|
||||
|
||||
public interface LuminousEnergy extends Quantity<LuminousEnergy> {
|
||||
}
|
||||
|
||||
public interface LuminousEfficacy extends Quantity<LuminousEfficacy> {
|
||||
}
|
||||
|
||||
public interface LuminousExposure extends Quantity<LuminousExposure> {
|
||||
}
|
||||
|
||||
public interface RadiantIntensity extends Quantity<RadiantIntensity> {
|
||||
}
|
||||
|
||||
public interface Radiance extends Quantity<Radiance> {
|
||||
}
|
||||
|
||||
public interface RadiationExposure extends Quantity<RadiationExposure> {
|
||||
}
|
||||
|
||||
public interface RadiationDoseAbsorptionRate extends Quantity<RadiationDoseAbsorptionRate> {
|
||||
}
|
||||
|
||||
public interface MassFlowRate extends Quantity<MassFlowRate> {
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.sputnikdev.bluetooth.gattparser.BluetoothGattParser;
|
||||
import org.sputnikdev.bluetooth.gattparser.BluetoothGattParserFactory;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.Enumerations;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.Field;
|
||||
|
||||
/**
|
||||
* {@link CharacteristicChannelTypeProvider} that provides channel types for dynamically discovered characteristics.
|
||||
*
|
||||
* @author Vlad Kolotov - Original author
|
||||
* @author Connor Petty - Modified for openHAB use.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { CharacteristicChannelTypeProvider.class, ChannelTypeProvider.class })
|
||||
public class CharacteristicChannelTypeProvider implements ChannelTypeProvider {
|
||||
|
||||
private static final String CHANNEL_TYPE_NAME_PATTERN = "characteristic-%s-%s-%s-%s";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CharacteristicChannelTypeProvider.class);
|
||||
|
||||
private final @NonNullByDefault({}) Map<ChannelTypeUID, ChannelType> cache = new ConcurrentHashMap<>();
|
||||
|
||||
private final BluetoothGattParser gattParser = BluetoothGattParserFactory.getDefault();
|
||||
|
||||
@Override
|
||||
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
|
||||
return cache.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
|
||||
if (isValidUID(channelTypeUID)) {
|
||||
return cache.computeIfAbsent(channelTypeUID, uid -> {
|
||||
String channelID = uid.getId();
|
||||
boolean advanced = "advncd".equals(channelID.substring(15, 21));
|
||||
boolean readOnly = "readable".equals(channelID.substring(22, 30));
|
||||
String characteristicUUID = channelID.substring(31, 67);
|
||||
String fieldName = channelID.substring(68, channelID.length());
|
||||
|
||||
if (gattParser.isKnownCharacteristic(characteristicUUID)) {
|
||||
List<Field> fields = gattParser.getFields(characteristicUUID).stream()
|
||||
.filter(field -> BluetoothChannelUtils.encodeFieldID(field).equals(fieldName))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (fields.size() > 1) {
|
||||
logger.warn("Multiple fields with the same name found: {} / {}. Skipping them.",
|
||||
characteristicUUID, fieldName);
|
||||
return null;
|
||||
}
|
||||
Field field = fields.get(0);
|
||||
return buildChannelType(uid, advanced, readOnly, field);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isValidUID(ChannelTypeUID channelTypeUID) {
|
||||
if (!channelTypeUID.getBindingId().equals(BluetoothBindingConstants.BINDING_ID)) {
|
||||
return false;
|
||||
}
|
||||
String channelID = channelTypeUID.getId();
|
||||
if (!channelID.startsWith("characteristic")) {
|
||||
return false;
|
||||
}
|
||||
if (channelID.length() < 68) {
|
||||
return false;
|
||||
}
|
||||
if (channelID.charAt(21) != '-') {
|
||||
return false;
|
||||
}
|
||||
if (channelID.charAt(30) != '-') {
|
||||
return false;
|
||||
}
|
||||
if (channelID.charAt(67) != '-') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public ChannelTypeUID registerChannelType(String characteristicUUID, boolean advanced, boolean readOnly,
|
||||
Field field) {
|
||||
// characteristic-advncd-readable-00002a04-0000-1000-8000-00805f9b34fb-Battery_Level
|
||||
String channelType = String.format(CHANNEL_TYPE_NAME_PATTERN, advanced ? "advncd" : "simple",
|
||||
readOnly ? "readable" : "writable", characteristicUUID, BluetoothChannelUtils.encodeFieldID(field));
|
||||
|
||||
ChannelTypeUID channelTypeUID = new ChannelTypeUID(BluetoothBindingConstants.BINDING_ID, channelType);
|
||||
cache.computeIfAbsent(channelTypeUID, uid -> buildChannelType(uid, advanced, readOnly, field));
|
||||
logger.debug("registered channel type: {}", channelTypeUID);
|
||||
return channelTypeUID;
|
||||
}
|
||||
|
||||
private ChannelType buildChannelType(ChannelTypeUID channelTypeUID, boolean advanced, boolean readOnly,
|
||||
Field field) {
|
||||
List<StateOption> options = getStateOptions(field);
|
||||
String itemType = BluetoothChannelUtils.getItemType(field);
|
||||
|
||||
if (itemType == null) {
|
||||
throw new IllegalStateException("Unknown field format type: " + field.getUnit());
|
||||
}
|
||||
|
||||
if (itemType.equals("Switch")) {
|
||||
options = Collections.emptyList();
|
||||
}
|
||||
|
||||
StateDescriptionFragmentBuilder stateDescBuilder = StateDescriptionFragmentBuilder.create()//
|
||||
.withPattern(getPattern(field))//
|
||||
.withReadOnly(readOnly)//
|
||||
.withOptions(options);
|
||||
|
||||
BigDecimal min = toBigDecimal(field.getMinimum());
|
||||
BigDecimal max = toBigDecimal(field.getMaximum());
|
||||
if (min != null) {
|
||||
stateDescBuilder = stateDescBuilder.withMinimum(min);
|
||||
}
|
||||
if (max != null) {
|
||||
stateDescBuilder = stateDescBuilder.withMaximum(max);
|
||||
}
|
||||
return ChannelTypeBuilder.state(channelTypeUID, field.getName(), itemType)//
|
||||
.isAdvanced(advanced)//
|
||||
.withDescription(field.getInformativeText())//
|
||||
.withStateDescriptionFragment(stateDescBuilder.build()).build();
|
||||
}
|
||||
|
||||
private static String getPattern(Field field) {
|
||||
String format = getFormat(field);
|
||||
String unit = getUnit(field);
|
||||
StringBuilder pattern = new StringBuilder();
|
||||
pattern.append(format);
|
||||
if (unit != null) {
|
||||
pattern.append(" ").append(unit);
|
||||
}
|
||||
return pattern.toString();
|
||||
}
|
||||
|
||||
private static List<StateOption> getStateOptions(Field field) {
|
||||
return Optional.ofNullable(field.getEnumerations())//
|
||||
.map(Enumerations::getEnumerations)//
|
||||
.stream()//
|
||||
.flatMap(List::stream)
|
||||
.map(enumeration -> new StateOption(String.valueOf(enumeration.getKey()), enumeration.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static @Nullable BigDecimal toBigDecimal(@Nullable Double value) {
|
||||
return value != null ? BigDecimal.valueOf(value) : null;
|
||||
}
|
||||
|
||||
private static String getFormat(Field field) {
|
||||
String format = "%s";
|
||||
Integer decimalExponent = field.getDecimalExponent();
|
||||
if (field.getFormat().isReal() && decimalExponent != null && decimalExponent < 0) {
|
||||
format = "%." + Math.abs(decimalExponent) + "f";
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
private static @Nullable String getUnit(Field field) {
|
||||
String gattUnit = field.getUnit();
|
||||
if (gattUnit != null) {
|
||||
BluetoothUnit unit = BluetoothUnit.findByType(gattUnit);
|
||||
if (unit != null) {
|
||||
return unit.getUnit().getSymbol();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GenericBindingConfiguration {
|
||||
|
||||
public int pollingInterval = 30;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link GenericBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GenericBindingConstants {
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID,
|
||||
"generic");
|
||||
|
||||
// Field properties
|
||||
public static final String PROPERTY_FIELD_NAME = "FieldName";
|
||||
public static final String PROPERTY_FIELD_INDEX = "FieldIndex";
|
||||
|
||||
// Characteristic properties
|
||||
public static final String PROPERTY_FLAGS = "Flags";
|
||||
public static final String PROPERTY_SERVICE_UUID = "ServiceUUID";
|
||||
public static final String PROPERTY_CHARACTERISTIC_UUID = "CharacteristicUUID";
|
||||
}
|
@ -0,0 +1,430 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.sputnikdev.bluetooth.gattparser.BluetoothGattParser;
|
||||
import org.sputnikdev.bluetooth.gattparser.BluetoothGattParserFactory;
|
||||
import org.sputnikdev.bluetooth.gattparser.FieldHolder;
|
||||
import org.sputnikdev.bluetooth.gattparser.GattRequest;
|
||||
import org.sputnikdev.bluetooth.gattparser.GattResponse;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.Characteristic;
|
||||
import org.sputnikdev.bluetooth.gattparser.spec.Field;
|
||||
|
||||
/**
|
||||
* This is a handler for generic connected bluetooth devices that dynamically generates
|
||||
* channels based off of a bluetooth device's GATT characteristics.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GenericBluetoothHandler.class);
|
||||
private final Map<BluetoothCharacteristic, CharacteristicHandler> charHandlers = new ConcurrentHashMap<>();
|
||||
private final Map<ChannelUID, CharacteristicHandler> channelHandlers = new ConcurrentHashMap<>();
|
||||
private final BluetoothGattParser gattParser = BluetoothGattParserFactory.getDefault();
|
||||
private final CharacteristicChannelTypeProvider channelTypeProvider;
|
||||
|
||||
private @Nullable ScheduledFuture<?> readCharacteristicJob = null;
|
||||
|
||||
public GenericBluetoothHandler(Thing thing, CharacteristicChannelTypeProvider channelTypeProvider) {
|
||||
super(thing);
|
||||
this.channelTypeProvider = channelTypeProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
super.initialize();
|
||||
|
||||
GenericBindingConfiguration config = getConfigAs(GenericBindingConfiguration.class);
|
||||
readCharacteristicJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
if (resolved) {
|
||||
for (CharacteristicHandler charHandler : charHandlers.values()) {
|
||||
if (charHandler.canRead()) {
|
||||
device.readCharacteristic(charHandler.characteristic);
|
||||
try {
|
||||
// TODO the ideal solution would be to use locks/conditions and timeouts
|
||||
// between this code and `onCharacteristicReadComplete` but
|
||||
// that would overcomplicate the code a bit and I plan
|
||||
// on implementing a better more generalized solution later
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if we are connected and still haven't been able to resolve the services, try disconnecting and
|
||||
// then connecting again
|
||||
device.disconnect();
|
||||
}
|
||||
}
|
||||
}, 15, config.pollingInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> future = readCharacteristicJob;
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
super.dispose();
|
||||
|
||||
charHandlers.clear();
|
||||
channelHandlers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
logger.trace("Service discovery completed for '{}'", address);
|
||||
updateThingChannels();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
CharacteristicHandler handler = channelHandlers.get(channelUID);
|
||||
if (handler != null) {
|
||||
handler.handleCommand(channelUID, command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicReadComplete(characteristic, status);
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
byte[] data = characteristic.getByteValue();
|
||||
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
byte[] data = characteristic.getByteValue();
|
||||
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
|
||||
}
|
||||
|
||||
private void updateThingChannels() {
|
||||
List<Channel> channels = device.getServices().stream()//
|
||||
.flatMap(service -> service.getCharacteristics().stream())//
|
||||
.flatMap(characteristic -> {
|
||||
logger.trace("{} processing characteristic {}", address, characteristic.getUuid());
|
||||
CharacteristicHandler handler = getCharacteristicHandler(characteristic);
|
||||
List<Channel> chans = handler.buildChannels();
|
||||
for (Channel channel : chans) {
|
||||
channelHandlers.put(channel.getUID(), handler);
|
||||
}
|
||||
return chans.stream();
|
||||
})//
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ThingBuilder builder = editThing();
|
||||
boolean changed = false;
|
||||
for (Channel channel : channels) {
|
||||
logger.trace("{} attempting to add channel {}", address, channel.getLabel());
|
||||
// we only want to add each channel, not replace all of them
|
||||
if (getThing().getChannel(channel.getUID()) == null) {
|
||||
changed = true;
|
||||
builder.withChannel(channel);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
updateThing(builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private CharacteristicHandler getCharacteristicHandler(BluetoothCharacteristic characteristic) {
|
||||
return charHandlers.computeIfAbsent(characteristic, CharacteristicHandler::new);
|
||||
}
|
||||
|
||||
private boolean readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
return device.readCharacteristic(characteristic);
|
||||
}
|
||||
|
||||
private boolean writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) {
|
||||
characteristic.setValue(data);
|
||||
return device.writeCharacteristic(characteristic);
|
||||
}
|
||||
|
||||
private class CharacteristicHandler {
|
||||
|
||||
private BluetoothCharacteristic characteristic;
|
||||
|
||||
public CharacteristicHandler(BluetoothCharacteristic characteristic) {
|
||||
this.characteristic = characteristic;
|
||||
}
|
||||
|
||||
private String getCharacteristicUUID() {
|
||||
return characteristic.getUuid().toString();
|
||||
}
|
||||
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
|
||||
// Handle REFRESH
|
||||
if (command == RefreshType.REFRESH) {
|
||||
if (canRead()) {
|
||||
readCharacteristic(characteristic);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// handle write
|
||||
if (command instanceof State) {
|
||||
State state = (State) command;
|
||||
String characteristicUUID = getCharacteristicUUID();
|
||||
try {
|
||||
if (gattParser.isKnownCharacteristic(characteristicUUID)) {
|
||||
String fieldName = getFieldName(channelUID);
|
||||
if (fieldName != null) {
|
||||
updateCharacteristic(fieldName, state);
|
||||
} else {
|
||||
logger.warn("Characteristic has no field name!");
|
||||
}
|
||||
} else if (state instanceof StringType) {
|
||||
// unknown characteristic
|
||||
byte[] data = HexUtils.hexToBytes(state.toString());
|
||||
if (!writeCharacteristic(characteristic, data)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not write data to characteristic: " + characteristicUUID);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not update bluetooth device. Error: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCharacteristic(String fieldName, State state) {
|
||||
// TODO maybe we should check if the characteristic is authenticated?
|
||||
String characteristicUUID = getCharacteristicUUID();
|
||||
|
||||
if (gattParser.isValidForWrite(characteristicUUID)) {
|
||||
GattRequest request = gattParser.prepare(characteristicUUID);
|
||||
try {
|
||||
BluetoothChannelUtils.updateHolder(gattParser, request, fieldName, state);
|
||||
byte[] data = gattParser.serialize(request);
|
||||
|
||||
if (!writeCharacteristic(characteristic, data)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not write data to characteristic: " + characteristicUUID);
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
logger.warn("Could not parse characteristic value: {} : {}", characteristicUUID, state, ex);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not parse characteristic value: " + characteristicUUID + " : " + state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleCharacteristicUpdate(byte[] data) {
|
||||
String characteristicUUID = getCharacteristicUUID();
|
||||
if (gattParser.isKnownCharacteristic(characteristicUUID)) {
|
||||
GattResponse response = gattParser.parse(characteristicUUID, data);
|
||||
for (FieldHolder holder : response.getFieldHolders()) {
|
||||
Field field = holder.getField();
|
||||
ChannelUID channelUID = getChannelUID(field);
|
||||
updateState(channelUID, BluetoothChannelUtils.convert(gattParser, holder));
|
||||
}
|
||||
} else {
|
||||
// this is a raw channel
|
||||
String hex = HexUtils.bytesToHex(data);
|
||||
ChannelUID channelUID = getChannelUID(null);
|
||||
updateState(channelUID, new StringType(hex));
|
||||
}
|
||||
}
|
||||
|
||||
public List<Channel> buildChannels() {
|
||||
List<Channel> channels = new ArrayList<>();
|
||||
String charUUID = getCharacteristicUUID();
|
||||
Characteristic gattChar = gattParser.getCharacteristic(charUUID);
|
||||
if (gattChar != null) {
|
||||
List<Field> fields = gattParser.getFields(charUUID);
|
||||
|
||||
String label = null;
|
||||
// check if the characteristic has only on field, if so use its name as label
|
||||
if (fields.size() == 1) {
|
||||
label = gattChar.getName();
|
||||
}
|
||||
|
||||
Map<String, List<Field>> fieldsMapping = fields.stream().collect(Collectors.groupingBy(Field::getName));
|
||||
|
||||
for (List<Field> fieldList : fieldsMapping.values()) {
|
||||
Field field = fieldList.get(0);
|
||||
if (fieldList.size() > 1) {
|
||||
if (field.isFlagField() || field.isOpCodesField()) {
|
||||
logger.debug("Skipping flags/op codes field: {}.", charUUID);
|
||||
} else {
|
||||
logger.warn("Multiple fields with the same name found: {} / {}. Skipping these fields.",
|
||||
charUUID, field.getName());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFieldSupported(field)) {
|
||||
Channel channel = buildFieldChannel(field, label, !gattChar.isValidForWrite());
|
||||
if (channel != null) {
|
||||
channels.add(channel);
|
||||
} else {
|
||||
logger.warn("Unable to build channel for field: {}", field.getName());
|
||||
}
|
||||
} else {
|
||||
logger.warn("GATT field is not supported: {} / {} / {}", charUUID, field.getName(),
|
||||
field.getFormat());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channels.add(buildUnknownChannel());
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
private Channel buildUnknownChannel() {
|
||||
ChannelUID channelUID = getChannelUID(null);
|
||||
ChannelTypeUID channelTypeUID = new ChannelTypeUID(BluetoothBindingConstants.BINDING_ID, "char-unknown");
|
||||
return ChannelBuilder.create(channelUID).withType(channelTypeUID).withProperties(getChannelProperties(null))
|
||||
.build();
|
||||
}
|
||||
|
||||
public boolean canRead() {
|
||||
String charUUID = getCharacteristicUUID();
|
||||
if (gattParser.isKnownCharacteristic(charUUID)) {
|
||||
return gattParser.isValidForRead(charUUID);
|
||||
}
|
||||
// TODO: need to evaluate this from characteristic properties, but such properties aren't support yet
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canWrite() {
|
||||
String charUUID = getCharacteristicUUID();
|
||||
if (gattParser.isKnownCharacteristic(charUUID)) {
|
||||
return gattParser.isValidForWrite(charUUID);
|
||||
}
|
||||
// TODO: need to evaluate this from characteristic properties, but such properties aren't support yet
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isAdvanced() {
|
||||
return !gattParser.isKnownCharacteristic(getCharacteristicUUID());
|
||||
}
|
||||
|
||||
private boolean isFieldSupported(Field field) {
|
||||
return field.getFormat() != null;
|
||||
}
|
||||
|
||||
private @Nullable Channel buildFieldChannel(Field field, @Nullable String charLabel, boolean readOnly) {
|
||||
String label = charLabel != null ? charLabel : field.getName();
|
||||
String acceptedType = BluetoothChannelUtils.getItemType(field);
|
||||
if (acceptedType == null) {
|
||||
// unknown field format
|
||||
return null;
|
||||
}
|
||||
|
||||
ChannelUID channelUID = getChannelUID(field);
|
||||
|
||||
logger.debug("Building a new channel for a field: {}", channelUID.getId());
|
||||
|
||||
ChannelTypeUID channelTypeUID = channelTypeProvider.registerChannelType(getCharacteristicUUID(),
|
||||
isAdvanced(), readOnly, field);
|
||||
|
||||
return ChannelBuilder.create(channelUID, acceptedType).withType(channelTypeUID)
|
||||
.withProperties(getChannelProperties(field.getName())).withLabel(label).build();
|
||||
}
|
||||
|
||||
private ChannelUID getChannelUID(@Nullable Field field) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("service-")//
|
||||
.append(toBluetoothHandle(characteristic.getService().getUuid()))//
|
||||
.append("-char-")//
|
||||
.append(toBluetoothHandle(characteristic.getUuid()));
|
||||
if (field != null) {
|
||||
builder.append("-").append(BluetoothChannelUtils.encodeFieldName(field.getName()));
|
||||
}
|
||||
return new ChannelUID(getThing().getUID(), builder.toString());
|
||||
}
|
||||
|
||||
private String toBluetoothHandle(UUID uuid) {
|
||||
long leastSig = uuid.getLeastSignificantBits();
|
||||
long mostSig = uuid.getMostSignificantBits();
|
||||
|
||||
if (leastSig == BluetoothBindingConstants.BLUETOOTH_BASE_UUID) {
|
||||
return "0x" + Long.toHexString(mostSig >> 32).toUpperCase();
|
||||
}
|
||||
return uuid.toString().toUpperCase();
|
||||
}
|
||||
|
||||
private @Nullable String getFieldName(ChannelUID channelUID) {
|
||||
String channelId = channelUID.getId();
|
||||
int index = channelId.lastIndexOf("-");
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException(
|
||||
"ChannelUID '" + channelUID + "' is not a valid GATT channel format");
|
||||
}
|
||||
String encodedFieldName = channelId.substring(index + 1);
|
||||
if (encodedFieldName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return BluetoothChannelUtils.decodeFieldName(encodedFieldName);
|
||||
}
|
||||
|
||||
private Map<String, String> getChannelProperties(@Nullable String fieldName) {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
if (fieldName != null) {
|
||||
properties.put(GenericBindingConstants.PROPERTY_FIELD_NAME, fieldName);
|
||||
}
|
||||
properties.put(GenericBindingConstants.PROPERTY_SERVICE_UUID,
|
||||
characteristic.getService().getUuid().toString());
|
||||
properties.put(GenericBindingConstants.PROPERTY_CHARACTERISTIC_UUID, getCharacteristicUUID());
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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.generic.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.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link GenericBluetoothHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.bluetooth.generic", service = ThingHandlerFactory.class)
|
||||
public class GenericBluetoothHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set
|
||||
.of(GenericBindingConstants.THING_TYPE_GENERIC);
|
||||
|
||||
private final CharacteristicChannelTypeProvider channelTypeProvider;
|
||||
|
||||
@Activate
|
||||
public GenericBluetoothHandlerFactory(@Reference CharacteristicChannelTypeProvider channelTypeProvider) {
|
||||
this.channelTypeProvider = channelTypeProvider;
|
||||
}
|
||||
|
||||
@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 (GenericBindingConstants.THING_TYPE_GENERIC.equals(thingTypeUID)) {
|
||||
return new GenericBluetoothHandler(thing, channelTypeProvider);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -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.generic.internal;
|
||||
|
||||
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.BluetoothCompanyIdentifiers;
|
||||
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 class implements the BluetoothDiscoveryParticipant for generic bluetooth devices.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = BluetoothDiscoveryParticipant.class)
|
||||
public class GenericDiscoveryParticipant implements BluetoothDiscoveryParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GenericDiscoveryParticipant.class);
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Set.of(GenericBindingConstants.THING_TYPE_GENERIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
|
||||
ThingUID thingUID = getThingUID(device);
|
||||
if (thingUID == null) {
|
||||
// the thingUID will never be null in practice but this makes the null checker happy
|
||||
return null;
|
||||
}
|
||||
String label = "Generic Connectable Bluetooth Device";
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
|
||||
Integer txPower = device.getTxPower();
|
||||
if (txPower != null && txPower > 0) {
|
||||
properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
|
||||
}
|
||||
String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
|
||||
if (manufacturer == null) {
|
||||
logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
|
||||
} else {
|
||||
properties.put(Thing.PROPERTY_VENDOR, manufacturer);
|
||||
label += " (" + manufacturer + ")";
|
||||
}
|
||||
|
||||
addPropertyIfPresent(properties, Thing.PROPERTY_MODEL_ID, device.getModel());
|
||||
addPropertyIfPresent(properties, Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber());
|
||||
addPropertyIfPresent(properties, Thing.PROPERTY_HARDWARE_VERSION, device.getHardwareRevision());
|
||||
addPropertyIfPresent(properties, Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareRevision());
|
||||
addPropertyIfPresent(properties, BluetoothBindingConstants.PROPERTY_SOFTWARE_VERSION,
|
||||
device.getSoftwareRevision());
|
||||
|
||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS)
|
||||
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
|
||||
}
|
||||
|
||||
private static void addPropertyIfPresent(Map<String, Object> properties, String key, @Nullable Object value) {
|
||||
if (value != null) {
|
||||
properties.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
|
||||
return new ThingUID(GenericBindingConstants.THING_TYPE_GENERIC, device.getAdapter().getUID(),
|
||||
device.getAddress().toString().toLowerCase().replace(":", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresConnection(BluetoothDiscoveryDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
// we want to go last
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?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="generic">
|
||||
<label>Generic Bluetooth Device</label>
|
||||
<description>A generic bluetooth device that supports GATT characteristics</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="address" type="text">
|
||||
<label>Address</label>
|
||||
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||
</parameter>
|
||||
<parameter name="pollingInterval" type="integer" unit="s">
|
||||
<advanced>true</advanced>
|
||||
<label>Polling Interval</label>
|
||||
<description>The frequency at which readable characteristics refreshed</description>
|
||||
<default>30</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="char-unknown">
|
||||
<item-type>String</item-type>
|
||||
<label>Unknown Bluetooth Characteristic</label>
|
||||
<description>The raw value of unknown characteristics are represented with hexadecimal</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BluetoothChannelUtilsTest {
|
||||
|
||||
@Test
|
||||
public void encodeDecodeFieldNameTest() {
|
||||
String str = "easure";
|
||||
assertEquals(str, BluetoothChannelUtils.decodeFieldName(BluetoothChannelUtils.encodeFieldName(str)));
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.generic.internal;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
class BluetoothUnitTest {
|
||||
|
||||
@Test
|
||||
void initializeTest() {
|
||||
BluetoothUnit.AMPERE.getUnit();
|
||||
}
|
||||
}
|
@ -17,23 +17,25 @@ This should be the best choice for any Linux-based single board computers like e
|
||||
|
||||
## Supported Things
|
||||
|
||||
Two thing types are supported by this binding:
|
||||
The base bluetooth binding only supports a single thing type.
|
||||
Additional thing types are available through bluetooth extensions.
|
||||
|
||||
| Thing Type ID | Description |
|
||||
|---------------|---------------------------------------------------------------------------------------------------------|
|
||||
| beacon | A Bluetooth device that is not connected, but only broadcasts announcements. |
|
||||
| connected | A Bluetooth device that allows a direct connection and which provides specific services when connected. |
|
||||
| beacon | A Bluetooth device that is not connected, but only broadcasts announcements. |
|
||||
|
||||
|
||||
## Discovery
|
||||
|
||||
Discovery is performed through the Bluetooth bridge.
|
||||
Normally, any broadcasting Bluetooth device can be uniquely identified and thus a bridge can create an inbox result for it.
|
||||
As this might lead to a huge list of devices, bridges usually also offer a way to deactivate this behavior.
|
||||
As this might lead to a huge list of devices, bridges usually disable this behavior by default.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
Both thing types only require a single configuration parameter `address`, which corresponds to the Bluetooth address of the device (in format "XX:XX:XX:XX:XX:XX").
|
||||
All bluetooth thing types require a configuration parameter `address`, which corresponds to the Bluetooth address of the device (in format "XX:XX:XX:XX:XX:XX").
|
||||
Other configuration parameters may be required depending on the bluetooth thing type, look at the documentation for that thing type for details.
|
||||
|
||||
|
||||
## Channels
|
||||
|
||||
@ -43,13 +45,6 @@ Every Bluetooth thing has the following channel:
|
||||
|------------|-----------|-----------------------------------------------------------------------------------------------------|
|
||||
| rssi | Number | The "Received Signal Strength Indicator", the [RSSI](https://blog.bluetooth.com/proximity-and-rssi) |
|
||||
|
||||
`connected` Things are dynamically queried for their services and if they support certain standard GATT characteristics, the appropriate channels are automatically added as well:
|
||||
|
||||
| Channel ID | Item Type | Description |
|
||||
|---------------|-----------|-----------------------------------------------------------------|
|
||||
| battery_level | Number | The device's battery level in percent |
|
||||
|
||||
|
||||
## Full Example
|
||||
|
||||
demo.things (assuming you have a Bluetooth bridge with the ID `bluetooth:bluez:hci0`):
|
||||
|
@ -30,7 +30,6 @@ public class BluetoothBindingConstants {
|
||||
public static final String BINDING_ID = "bluetooth";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_CONNECTED = new ThingTypeUID(BINDING_ID, "connected");
|
||||
public static final ThingTypeUID THING_TYPE_BEACON = new ThingTypeUID(BINDING_ID, "beacon");
|
||||
|
||||
// List of all Channel Type IDs
|
||||
@ -40,6 +39,7 @@ public class BluetoothBindingConstants {
|
||||
|
||||
public static final String PROPERTY_TXPOWER = "txpower";
|
||||
public static final String PROPERTY_MAXCONNECTIONS = "maxconnections";
|
||||
public static final String PROPERTY_SOFTWARE_VERSION = "softwareVersion";
|
||||
|
||||
public static final String CONFIGURATION_ADDRESS = "address";
|
||||
public static final String CONFIGURATION_DISCOVERY = "backgroundDiscovery";
|
||||
|
@ -221,6 +221,48 @@ public class BluetoothCharacteristic {
|
||||
return gattDescriptors.get(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + instance;
|
||||
result = prime * result + ((service == null) ? 0 : service.hashCode());
|
||||
result = prime * result + ((uuid == null) ? 0 : uuid.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
BluetoothCharacteristic other = (BluetoothCharacteristic) obj;
|
||||
if (instance != other.instance) {
|
||||
return false;
|
||||
}
|
||||
if (service == null) {
|
||||
if (other.service != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!service.equals(other.service)) {
|
||||
return false;
|
||||
}
|
||||
if (uuid == null) {
|
||||
if (other.uuid != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!uuid.equals(other.uuid)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored value for this characteristic.
|
||||
*
|
||||
|
@ -19,20 +19,12 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
|
||||
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.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
@ -40,11 +32,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This is a handler for generic Bluetooth devices in connected mode, which at the same time can be used
|
||||
* as a base implementation for more specific thing handlers.
|
||||
* This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.ARRAY_CONTENTS,
|
||||
DefaultLocation.TYPE_ARGUMENT, DefaultLocation.TYPE_BOUND, DefaultLocation.TYPE_PARAMETER })
|
||||
@ -67,11 +57,21 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
super.initialize();
|
||||
|
||||
connectionJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
device.connect();
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to onConnectionStateChange
|
||||
try {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
device.connect();
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to
|
||||
// onConnectionStateChange
|
||||
} else {
|
||||
// just in case it was already connected to begin with
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!resolved && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Unexpected error occurred", ex);
|
||||
}
|
||||
updateRSSI();
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@ -81,18 +81,7 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
connectionJob.cancel(true);
|
||||
connectionJob = null;
|
||||
}
|
||||
scheduler.submit(() -> {
|
||||
try {
|
||||
deviceLock.lock();
|
||||
if (device != null) {
|
||||
device.removeListener(this);
|
||||
device.disconnect();
|
||||
device = null;
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -167,12 +156,6 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
logger.debug("Service discovery completed for '{}'", address);
|
||||
BluetoothCharacteristic characteristic = device
|
||||
.getCharacteristic(GattCharacteristic.BATTERY_LEVEL.getUUID());
|
||||
if (characteristic != null) {
|
||||
activateChannel(characteristic, DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID());
|
||||
logger.debug("Added GATT characteristic '{}'", characteristic.getGattCharacteristic().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,13 +163,9 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicReadComplete(characteristic, status);
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
if (GattCharacteristic.BATTERY_LEVEL.equals(characteristic.getGattCharacteristic())) {
|
||||
updateBatteryLevel(characteristic);
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(),
|
||||
address, HexUtils.bytesToHex(characteristic.getByteValue()));
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()));
|
||||
}
|
||||
} else {
|
||||
logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
|
||||
@ -210,9 +189,6 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
logger.debug("Recieved update {} to characteristic {} of device {}",
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
|
||||
}
|
||||
if (GattCharacteristic.BATTERY_LEVEL.equals(characteristic.getGattCharacteristic())) {
|
||||
updateBatteryLevel(characteristic);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -223,41 +199,4 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
descriptor.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateBatteryLevel(BluetoothCharacteristic characteristic) {
|
||||
// the byte has values from 0-255, which we need to map to 0-100
|
||||
Double level = characteristic.getValue()[0] / 2.55;
|
||||
updateState(characteristic.getGattCharacteristic().name(), new DecimalType(level.intValue()));
|
||||
}
|
||||
|
||||
protected void activateChannel(@Nullable BluetoothCharacteristic characteristic, ChannelTypeUID channelTypeUID,
|
||||
@Nullable String name) {
|
||||
if (characteristic != null) {
|
||||
String channelId = name != null ? name : characteristic.getGattCharacteristic().name();
|
||||
if (channelId == null) {
|
||||
// use the type id as a fallback
|
||||
channelId = channelTypeUID.getId();
|
||||
}
|
||||
if (getThing().getChannel(channelId) == null) {
|
||||
// the channel does not exist yet, so let's add it
|
||||
ThingBuilder updatedThing = editThing();
|
||||
Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), "Number")
|
||||
.withType(channelTypeUID).build();
|
||||
updatedThing.withChannel(channel);
|
||||
updateThing(updatedThing.build());
|
||||
logger.debug("Added channel '{}' to Thing '{}'", channelId, getThing().getUID());
|
||||
}
|
||||
deviceCharacteristics.add(characteristic);
|
||||
device.enableNotifications(characteristic);
|
||||
if (isLinked(channelId)) {
|
||||
device.readCharacteristic(characteristic);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Characteristic is null - not activating any channel.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void activateChannel(@Nullable BluetoothCharacteristic characteristic, ChannelTypeUID channelTypeUID) {
|
||||
activateChannel(characteristic, channelTypeUID, null);
|
||||
}
|
||||
}
|
||||
|
@ -91,4 +91,14 @@ public interface BluetoothDiscoveryParticipant {
|
||||
BiConsumer<BluetoothAdapter, DiscoveryResult> publisher) {
|
||||
// do nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriding this method allows discovery participants to dictate the order in which they should be evaluated
|
||||
* relative to other discovery participants. Participants with a lower order value are evaluated first.
|
||||
*
|
||||
* @return the order of this participant, default 0
|
||||
*/
|
||||
public default int order() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ package org.openhab.binding.bluetooth.discovery.internal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -35,7 +36,6 @@ import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.BluetoothDeviceListener;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
|
||||
@ -44,6 +44,7 @@ import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -86,9 +87,12 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
|
||||
@Override
|
||||
public DiscoveryResult get() {
|
||||
List<BluetoothDiscoveryParticipant> sortedParticipants = new ArrayList<>(participants);
|
||||
sortedParticipants.sort(Comparator.comparing(BluetoothDiscoveryParticipant::order));
|
||||
|
||||
// first see if any of the participants that don't require a connection recognize this device
|
||||
List<BluetoothDiscoveryParticipant> connectionParticipants = new ArrayList<>();
|
||||
for (BluetoothDiscoveryParticipant participant : participants) {
|
||||
for (BluetoothDiscoveryParticipant participant : sortedParticipants) {
|
||||
if (participant.requiresConnection(device)) {
|
||||
connectionParticipants.add(participant);
|
||||
continue;
|
||||
@ -105,25 +109,23 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
|
||||
// Since we couldn't find a result, lets try the connection based participants
|
||||
DiscoveryResult result = null;
|
||||
if (!connectionParticipants.isEmpty()) {
|
||||
BluetoothAddress address = device.getAddress();
|
||||
if (isAddressAvailable(address)) {
|
||||
result = findConnectionResult(connectionParticipants);
|
||||
// make sure to disconnect before letting go of the device
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
try {
|
||||
if (!device.disconnect()) {
|
||||
logger.debug("Failed to disconnect from device {}", address);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
|
||||
device.getAdapter().getUID(), ex);
|
||||
BluetoothAddress address = device.getAddress();
|
||||
if (isAddressAvailable(address)) {
|
||||
result = findConnectionResult(connectionParticipants);
|
||||
// make sure to disconnect before letting go of the device
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
try {
|
||||
if (!device.disconnect()) {
|
||||
logger.debug("Failed to disconnect from device {}", address);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
|
||||
device.getAdapter().getUID(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = createDefaultResult(device);
|
||||
result = createDefaultResult();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -133,8 +135,8 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
return adapters.stream().noneMatch(adapter -> adapter.hasHandlerForDevice(address));
|
||||
}
|
||||
|
||||
private DiscoveryResult createDefaultResult(BluetoothDevice device) {
|
||||
// We did not find a thing type for this device, so let's treat it as a generic one
|
||||
private DiscoveryResult createDefaultResult() {
|
||||
// We did not find a thing type for this device, so let's treat it as a generic beacon
|
||||
String label = device.getName();
|
||||
if (label == null || label.length() == 0 || label.equals(device.getAddress().toString().replace(':', '-'))) {
|
||||
label = "Bluetooth Device";
|
||||
@ -154,42 +156,51 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
label += " (" + manufacturer + ")";
|
||||
}
|
||||
|
||||
ThingUID thingUID = new ThingUID(BluetoothBindingConstants.THING_TYPE_BEACON, device.getAdapter().getUID(),
|
||||
device.getAddress().toString().toLowerCase().replace(":", ""));
|
||||
ThingTypeUID thingTypeUID = BluetoothBindingConstants.THING_TYPE_BEACON;
|
||||
|
||||
ThingUID thingUID = new ThingUID(thingTypeUID, device.getAdapter().getUID(),
|
||||
device.getAddress().toString().toLowerCase().replace(":", ""));
|
||||
// Create the discovery result and add to the inbox
|
||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS).withTTL(DISCOVERY_TTL)
|
||||
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
|
||||
}
|
||||
|
||||
// this is really just a special return type for `ensureConnected`
|
||||
private static class ConnectionException extends Exception {
|
||||
|
||||
}
|
||||
|
||||
private void ensureConnected() throws ConnectionException, InterruptedException {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
|
||||
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
throw new ConnectionException();
|
||||
}
|
||||
if (!awaitConnection(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Connection to device {} timed out", device.getAddress());
|
||||
throw new ConnectionException();
|
||||
}
|
||||
if (!servicesDiscovered) {
|
||||
device.discoverServices();
|
||||
if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Service discovery for device {} timed out", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
throw new ConnectionException();
|
||||
}
|
||||
}
|
||||
readDeviceInformationIfMissing();
|
||||
logger.debug("Device information fetched from the device: {}", device);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
|
||||
try {
|
||||
device.addListener(this);
|
||||
for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
|
||||
// we call this every time just in case a participant somehow closes the connection
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
|
||||
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
return null;
|
||||
}
|
||||
if (!awaitConnection(1, TimeUnit.SECONDS)) {
|
||||
logger.debug("Connection to device {} timed out", device.getAddress());
|
||||
return null;
|
||||
}
|
||||
if (!servicesDiscovered) {
|
||||
device.discoverServices();
|
||||
if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Service discovery for device {} timed out", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
return null;
|
||||
}
|
||||
}
|
||||
readDeviceInformationIfMissing();
|
||||
logger.debug("Device information fetched from the device: {}", device);
|
||||
}
|
||||
|
||||
ensureConnected();
|
||||
try {
|
||||
DiscoveryResult result = participant.createResult(device);
|
||||
if (result != null) {
|
||||
@ -199,7 +210,7 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
} catch (InterruptedException | ConnectionException e) {
|
||||
// do nothing
|
||||
} finally {
|
||||
device.removeListener(this);
|
||||
|
@ -19,7 +19,6 @@ 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.ConnectedBluetoothHandler;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
@ -39,7 +38,6 @@ public class BluetoothHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>();
|
||||
static {
|
||||
SUPPORTED_THING_TYPES_UIDS.add(BluetoothBindingConstants.THING_TYPE_BEACON);
|
||||
SUPPORTED_THING_TYPES_UIDS.add(BluetoothBindingConstants.THING_TYPE_CONNECTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -53,8 +51,6 @@ public class BluetoothHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
if (thingTypeUID.equals(BluetoothBindingConstants.THING_TYPE_BEACON)) {
|
||||
return new BeaconBluetoothHandler(thing);
|
||||
} else if (thingTypeUID.equals(BluetoothBindingConstants.THING_TYPE_CONNECTED)) {
|
||||
return new ConnectedBluetoothHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -59,6 +59,7 @@
|
||||
<module>org.openhab.binding.bluetooth.bluez</module>
|
||||
<module>org.openhab.binding.bluetooth.blukii</module>
|
||||
<module>org.openhab.binding.bluetooth.daikinmadoka</module>
|
||||
<module>org.openhab.binding.bluetooth.generic</module>
|
||||
<module>org.openhab.binding.bluetooth.roaming</module>
|
||||
<module>org.openhab.binding.bluetooth.ruuvitag</module>
|
||||
<module>org.openhab.binding.boschindego</module>
|
||||
|
@ -6,12 +6,13 @@
|
||||
<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.airthings/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.am43/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.blukii/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluez/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluegiga/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.blukii/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.daikinmadoka/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.roaming/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version}</bundle>
|
||||
</feature>
|
||||
<feature name="openhab-binding-mqtt" description="MQTT Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
|
Loading…
Reference in New Issue
Block a user