]> git.basschouten.com Git - openhab-addons.git/commitdiff
[bluetooth.generic] Added support for generic bluetooth devices (#8775)
authorConnor Petty <mistercpp2000@gmail.com>
Mon, 23 Nov 2020 09:43:44 +0000 (01:43 -0800)
committerGitHub <noreply@github.com>
Mon, 23 Nov 2020 09:43:44 +0000 (10:43 +0100)
* Generic Bluetooth Binding Initial Contribution

Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
26 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.bluetooth.generic/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/README.md [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnit.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/CharacteristicChannelTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericDiscoveryParticipant.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtilsTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnitTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth/README.md
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/BluetoothDiscoveryParticipant.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/internal/BluetoothHandlerFactory.java
bundles/pom.xml
features/openhab-addons/src/main/resources/footer.xml

index 45dfce187bfa5471b9f3b37c96d54378360f2a16..750a2e8bf9ac286abc14c9825a509210c216b89c 100644 (file)
@@ -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
index a64b92e1ca5c567fae6eba95b028424c983c19c7..b235f328d21d1dc9a635dd5f1ada09b66ebe2368 100644 (file)
       <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>
diff --git a/bundles/org.openhab.binding.bluetooth.generic/NOTICE b/bundles/org.openhab.binding.bluetooth.generic/NOTICE
new file mode 100644 (file)
index 0000000..dbc0cfc
--- /dev/null
@@ -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
diff --git a/bundles/org.openhab.binding.bluetooth.generic/README.md b/bundles/org.openhab.binding.bluetooth.generic/README.md
new file mode 100644 (file)
index 0000000..4b6be9a
--- /dev/null
@@ -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.
diff --git a/bundles/org.openhab.binding.bluetooth.generic/pom.xml b/bundles/org.openhab.binding.bluetooth.generic/pom.xml
new file mode 100644 (file)
index 0000000..488e91f
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/feature/feature.xml b/bundles/org.openhab.binding.bluetooth.generic/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..0d00579
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtils.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtils.java
new file mode 100644 (file)
index 0000000..47ac1f0
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnit.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnit.java
new file mode 100644 (file)
index 0000000..3ad8494
--- /dev/null
@@ -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> {
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/CharacteristicChannelTypeProvider.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/CharacteristicChannelTypeProvider.java
new file mode 100644 (file)
index 0000000..3ebab84
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConfiguration.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConfiguration.java
new file mode 100644 (file)
index 0000000..1d7e7fd
--- /dev/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;
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConstants.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConstants.java
new file mode 100644 (file)
index 0000000..a9d7357
--- /dev/null
@@ -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";
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java
new file mode 100644 (file)
index 0000000..4fd4b20
--- /dev/null
@@ -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;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandlerFactory.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandlerFactory.java
new file mode 100644 (file)
index 0000000..13b5130
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericDiscoveryParticipant.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericDiscoveryParticipant.java
new file mode 100644 (file)
index 0000000..22e285d
--- /dev/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;
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml b/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml
new file mode 100644 (file)
index 0000000..0f51494
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtilsTest.java b/bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtilsTest.java
new file mode 100644 (file)
index 0000000..f78e22b
--- /dev/null
@@ -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)));
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnitTest.java b/bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnitTest.java
new file mode 100644 (file)
index 0000000..a977d89
--- /dev/null
@@ -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();
+    }
+}
index 8a78be4b917452a08b852cf943ea16611d2623d3..51360c4e54526e72e97007b3c03d94e792917e81 100644 (file)
@@ -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`):
index 7eea293c76fef76d246fd70f7b1a25e112bcf260..a258601dffc1ab75eea4157dea6cba35cc11a2c3 100644 (file)
@@ -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";
index e3bbf466351d685cf34024007eb641b833e72951..36d61def615dbe1765ffb074520cacae01807808 100644 (file)
@@ -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.
      *
index 370e3bc2dea436edee41a8991696fb65e935fa66..b59d3dd425d89c99da977eb783b5288cad302c95 100644 (file)
@@ -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);
-    }
 }
index 1378a2cc0149743e3db8768d99fc867f5faf6559..9b8c95150e32a79cb5e840c2124205ed6a063f5a 100644 (file)
@@ -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;
+    }
 }
index 9924a5012ac3bf60832a12d6717ff782de08d3cf..a5a55bb488347d88d4fda3bd125992310a663a38 100644 (file)
@@ -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);
index 18c7bd8f71f88214157aa32995d57d62a22be957..ec1ce94ab27361e3ee3be33ba22fa5cd7729249d 100644 (file)
@@ -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;
     }
index 8e46d1fc4618c343f498736f6d16301d06118270..51e709f2d97b819fc35061362515ca1ed1c18d64 100644 (file)
@@ -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>
index eeeabd0fd1dc92c351c5a66cdfc4b0f3351cd567..9180ad4c7614680d5b0c8bd3a56cac45a155825b 100644 (file)
@@ -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>