diff --git a/CODEOWNERS b/CODEOWNERS index 45dfce187bf..750a2e8bf9a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index a64b92e1ca5..b235f328d21 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -126,6 +126,11 @@ org.openhab.binding.bluetooth.daikinmadoka ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.bluetooth.generic + ${project.version} + org.openhab.addons.bundles org.openhab.binding.bluetooth.roaming diff --git a/bundles/org.openhab.binding.bluetooth.generic/NOTICE b/bundles/org.openhab.binding.bluetooth.generic/NOTICE new file mode 100644 index 00000000000..dbc0cfc144e --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/NOTICE @@ -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 index 00000000000..4b6be9af5dd --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/README.md @@ -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 index 00000000000..488e91fb138 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/pom.xml @@ -0,0 +1,44 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.bluetooth.generic + + openHAB Add-ons :: Bundles :: Generic Bluetooth Adapter + + + + org.openhab.addons.bundles + org.openhab.binding.bluetooth + ${project.version} + provided + + + org.sputnikdev + bluetooth-gatt-parser + 1.9.4 + compile + + + commons-collections + commons-collections + 3.2.2 + compile + + + commons-beanutils + commons-beanutils + 1.9.3 + compile + + + + 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 index 00000000000..0d0057915fa --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/feature/feature.xml @@ -0,0 +1,24 @@ + + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version} + + 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 index 00000000000..47ac1f01c4b --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtils.java @@ -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 @Nullable T convert(State state, Class 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 index 00000000000..3ad8494e6e6 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnit.java @@ -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 KILOGRAM_PER_SQUARE_METER = addUnit( + new ProductUnit(Units.KILOGRAM.divide(Units.SQUARE_METRE))); + + public static final Unit COULOMB_PER_KILOGRAM = addUnit( + new ProductUnit(Units.COULOMB.divide(Units.KILOGRAM))); + + public static final Unit GRAY_PER_SECOND = addUnit( + new ProductUnit(Units.GRAY.divide(Units.SECOND))); + + public static final Unit POUND = addUnit( + new TransformedUnit(Units.KILOGRAM, new MultiplyConverter(0.45359237))); + + public static final Unit MINUTE_ANGLE = addUnit(new TransformedUnit(Units.RADIAN, + new PiMultiplierConverter().concatenate(new RationalConverter(1, 180 * 60)))); + + public static final Unit SECOND_ANGLE = addUnit(new TransformedUnit(Units.RADIAN, + new PiMultiplierConverter().concatenate(new RationalConverter(1, 180 * 60 * 60)))); + + public static final Unit HECTARE = addUnit(Units.SQUARE_METRE.multiply(10000.0)); + public static final Unit BARN = addUnit(Units.SQUARE_METRE.multiply(10E-28)); + + public static final Unit NAUTICAL_MILE = addUnit(SIUnits.METRE.multiply(1852.0)); + + public static final Unit WATT_PER_STERADIAN = addUnit( + new ProductUnit(Units.WATT.divide(Units.STERADIAN))); + + public static final Unit WATT_PER_STERADIAN_PER_SQUARE_METRE = addUnit( + new ProductUnit(WATT_PER_STERADIAN.divide(Units.SQUARE_METRE))); + + public static final Unit CYCLES_PER_MINUTE = addUnit(new TransformedUnit(Units.HERTZ, + new RationalConverter(BigInteger.valueOf(60), BigInteger.ONE))); + + public static final Unit REVOLUTION = addUnit(new TransformedUnit(Units.RADIAN, + new PiMultiplierConverter().concatenate(new RationalConverter(2, 1)))); + public static final Unit REVOLUTION_PER_MINUTE = addUnit( + new ProductUnit(REVOLUTION.divide(Units.MINUTE))); + + public static final Unit STEPS = addUnit(SmartHomeUnits.ONE.alternate("steps")); + public static final Unit BEATS = addUnit(SmartHomeUnits.ONE.alternate("beats")); + public static final Unit STROKE = addUnit(SmartHomeUnits.ONE.alternate("stroke")); + + public static final Unit STEP_PER_MINUTE = addUnit( + new ProductUnit(STEPS.divide(Units.MINUTE))); + + public static final Unit BEATS_PER_MINUTE = addUnit( + new ProductUnit(BEATS.divide(Units.MINUTE))); + + public static final Unit STROKE_PER_MINUTE = addUnit( + new ProductUnit(STROKE.divide(Units.MINUTE))); + + public static final Unit GRAM_PER_SECOND = addUnit( + new ProductUnit(Units.GRAM.divide(Units.SECOND))); + + public static final Unit LUMEN_PER_WATT = addUnit( + new ProductUnit(Units.LUMEN.divide(Units.WATT))); + + public static final Unit LUMEN_SECOND = addUnit( + new ProductUnit(Units.LUMEN.multiply(Units.SECOND))); + + public static final Unit LUMEN_HOUR = addUnit( + new ProductUnit(Units.LUMEN.multiply(Units.HOUR))); + + public static final Unit AMPERE_HOUR = addUnit( + new ProductUnit(Units.AMPERE.multiply(Units.HOUR))); + + public static final Unit LUX_HOUR = addUnit( + new ProductUnit(Units.LUX.multiply(Units.HOUR))); + + public static final Unit KILOMETRE_PER_MINUTE = addUnit(Units.KILOMETRE_PER_HOUR.multiply(60.0)); + + public static final Unit LITRE_PER_SECOND = addUnit( + new ProductUnit(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 addUnit(U unit) { + return unit; + } + + public interface AngularVelocity extends Quantity { + } + + public interface LuminousEnergy extends Quantity { + } + + public interface LuminousEfficacy extends Quantity { + } + + public interface LuminousExposure extends Quantity { + } + + public interface RadiantIntensity extends Quantity { + } + + public interface Radiance extends Quantity { + } + + public interface RadiationExposure extends Quantity { + } + + public interface RadiationDoseAbsorptionRate extends Quantity { + } + + public interface MassFlowRate extends Quantity { + } + } +} 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 index 00000000000..3ebab84aeb7 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/CharacteristicChannelTypeProvider.java @@ -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 cache = new ConcurrentHashMap<>(); + + private final BluetoothGattParser gattParser = BluetoothGattParserFactory.getDefault(); + + @Override + public Collection 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 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 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 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 index 00000000000..1d7e7fd62bd --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConfiguration.java @@ -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 index 00000000000..a9d73573b42 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBindingConstants.java @@ -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 index 00000000000..4fd4b206bb6 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java @@ -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 charHandlers = new ConcurrentHashMap<>(); + private final Map 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 channels = device.getServices().stream()// + .flatMap(service -> service.getCharacteristics().stream())// + .flatMap(characteristic -> { + logger.trace("{} processing characteristic {}", address, characteristic.getUuid()); + CharacteristicHandler handler = getCharacteristicHandler(characteristic); + List 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 buildChannels() { + List channels = new ArrayList<>(); + String charUUID = getCharacteristicUUID(); + Characteristic gattChar = gattParser.getCharacteristic(charUUID); + if (gattChar != null) { + List 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> fieldsMapping = fields.stream().collect(Collectors.groupingBy(Field::getName)); + + for (List 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 getChannelProperties(@Nullable String fieldName) { + Map 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 index 00000000000..13b51309982 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandlerFactory.java @@ -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 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 index 00000000000..22e285dca11 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericDiscoveryParticipant.java @@ -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 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 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 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 index 00000000000..0f51494318f --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml @@ -0,0 +1,32 @@ + + + + + + A generic bluetooth device that supports GATT characteristics + + + + + Bluetooth address in XX:XX:XX:XX:XX:XX format + + + true + + The frequency at which readable characteristics refreshed + 30 + + + + + + + String + + The raw value of unknown characteristics are represented with hexadecimal + + + 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 index 00000000000..f78e22bfdc0 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothChannelUtilsTest.java @@ -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 index 00000000000..a977d89ff3a --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.generic/src/test/java/org/openhab/binding/bluetooth/generic/internal/BluetoothUnitTest.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.bluetooth/README.md b/bundles/org.openhab.binding.bluetooth/README.md index 8a78be4b917..51360c4e545 100644 --- a/bundles/org.openhab.binding.bluetooth/README.md +++ b/bundles/org.openhab.binding.bluetooth/README.md @@ -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`): diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java index 7eea293c76f..a258601dffc 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java index e3bbf466351..36d61def615 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java @@ -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. * diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java index 370e3bc2dea..b59d3dd425d 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java @@ -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); - } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/BluetoothDiscoveryParticipant.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/BluetoothDiscoveryParticipant.java index 1378a2cc014..9b8c95150e3 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/BluetoothDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/BluetoothDiscoveryParticipant.java @@ -91,4 +91,14 @@ public interface BluetoothDiscoveryParticipant { BiConsumer 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; + } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java index 9924a5012ac..a5a55bb4883 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java @@ -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, Blu @Override public DiscoveryResult get() { + List 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 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, 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, 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, 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 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, Blu logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e); } } - } catch (InterruptedException e) { + } catch (InterruptedException | ConnectionException e) { // do nothing } finally { device.removeListener(this); diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/internal/BluetoothHandlerFactory.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/internal/BluetoothHandlerFactory.java index 18c7bd8f71f..ec1ce94ab27 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/internal/BluetoothHandlerFactory.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/internal/BluetoothHandlerFactory.java @@ -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 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; } diff --git a/bundles/pom.xml b/bundles/pom.xml index 8e46d1fc461..51e709f2d97 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -59,6 +59,7 @@ org.openhab.binding.bluetooth.bluez org.openhab.binding.bluetooth.blukii org.openhab.binding.bluetooth.daikinmadoka + org.openhab.binding.bluetooth.generic org.openhab.binding.bluetooth.roaming org.openhab.binding.bluetooth.ruuvitag org.openhab.binding.boschindego diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml index eeeabd0fd1d..9180ad4c761 100644 --- a/features/openhab-addons/src/main/resources/footer.xml +++ b/features/openhab-addons/src/main/resources/footer.xml @@ -6,12 +6,13 @@ mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.airthings/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.am43/${project.version} - mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.blukii/${project.version} - mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluez/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluegiga/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.blukii/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.daikinmadoka/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.roaming/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version} openhab-runtime-base