2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.generic.internal;
15 import java.math.BigDecimal;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.List;
19 import java.util.Locale;
21 import java.util.Optional;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.stream.Collectors;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
28 import org.openhab.bluetooth.gattparser.BluetoothGattParser;
29 import org.openhab.bluetooth.gattparser.BluetoothGattParserFactory;
30 import org.openhab.bluetooth.gattparser.spec.Enumerations;
31 import org.openhab.bluetooth.gattparser.spec.Field;
32 import org.openhab.core.thing.type.ChannelType;
33 import org.openhab.core.thing.type.ChannelTypeBuilder;
34 import org.openhab.core.thing.type.ChannelTypeProvider;
35 import org.openhab.core.thing.type.ChannelTypeUID;
36 import org.openhab.core.types.StateDescriptionFragmentBuilder;
37 import org.openhab.core.types.StateOption;
38 import org.osgi.service.component.annotations.Component;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * {@link CharacteristicChannelTypeProvider} that provides channel types for dynamically discovered characteristics.
45 * @author Vlad Kolotov - Original author
46 * @author Connor Petty - Modified for openHAB use.
49 @Component(service = { CharacteristicChannelTypeProvider.class, ChannelTypeProvider.class })
50 public class CharacteristicChannelTypeProvider implements ChannelTypeProvider {
52 private static final String CHANNEL_TYPE_NAME_PATTERN = "characteristic-%s-%s-%s-%s";
54 private final Logger logger = LoggerFactory.getLogger(CharacteristicChannelTypeProvider.class);
56 private final @NonNullByDefault({}) Map<ChannelTypeUID, ChannelType> cache = new ConcurrentHashMap<>();
58 private final BluetoothGattParser gattParser = BluetoothGattParserFactory.getDefault();
61 public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
62 return cache.values();
66 public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
67 if (isValidUID(channelTypeUID)) {
68 return cache.computeIfAbsent(channelTypeUID, uid -> {
69 String channelID = uid.getId();
70 boolean advanced = "advncd".equals(channelID.substring(15, 21));
71 boolean readOnly = "readable".equals(channelID.substring(22, 30));
72 String characteristicUUID = channelID.substring(31, 67);
73 String fieldName = channelID.substring(68, channelID.length());
75 if (gattParser.isKnownCharacteristic(characteristicUUID)) {
76 List<Field> fields = gattParser.getFields(characteristicUUID).stream()
77 .filter(field -> BluetoothChannelUtils.encodeFieldID(field).equals(fieldName))
78 .collect(Collectors.toList());
80 if (fields.size() > 1) {
81 logger.warn("Multiple fields with the same name found: {} / {}. Skipping them.",
82 characteristicUUID, fieldName);
85 Field field = fields.get(0);
86 return buildChannelType(uid, advanced, readOnly, field);
94 private static boolean isValidUID(ChannelTypeUID channelTypeUID) {
95 if (!channelTypeUID.getBindingId().equals(BluetoothBindingConstants.BINDING_ID)) {
98 String channelID = channelTypeUID.getId();
99 if (!channelID.startsWith("characteristic")) {
102 if (channelID.length() < 68) {
105 if (channelID.charAt(21) != '-') {
108 if (channelID.charAt(30) != '-') {
111 return channelID.charAt(67) == '-';
114 public ChannelTypeUID registerChannelType(String characteristicUUID, boolean advanced, boolean readOnly,
116 // characteristic-advncd-readable-00002a04-0000-1000-8000-00805f9b34fb-Battery_Level
117 String channelType = String.format(CHANNEL_TYPE_NAME_PATTERN, advanced ? "advncd" : "simple",
118 readOnly ? "readable" : "writable", characteristicUUID, BluetoothChannelUtils.encodeFieldID(field));
120 ChannelTypeUID channelTypeUID = new ChannelTypeUID(BluetoothBindingConstants.BINDING_ID, channelType);
121 cache.computeIfAbsent(channelTypeUID, uid -> buildChannelType(uid, advanced, readOnly, field));
122 logger.debug("registered channel type: {}", channelTypeUID);
123 return channelTypeUID;
126 private ChannelType buildChannelType(ChannelTypeUID channelTypeUID, boolean advanced, boolean readOnly,
128 List<StateOption> options = getStateOptions(field);
129 String itemType = BluetoothChannelUtils.getItemType(field);
131 if (itemType == null) {
132 throw new IllegalStateException("Unknown field format type: " + field.getUnit());
135 if ("Switch".equals(itemType)) {
136 options = Collections.emptyList();
139 StateDescriptionFragmentBuilder stateDescBuilder = StateDescriptionFragmentBuilder.create()//
140 .withPattern(getPattern(field))//
141 .withReadOnly(readOnly)//
142 .withOptions(options);
144 BigDecimal min = toBigDecimal(field.getMinimum());
145 BigDecimal max = toBigDecimal(field.getMaximum());
147 stateDescBuilder = stateDescBuilder.withMinimum(min);
150 stateDescBuilder = stateDescBuilder.withMaximum(max);
152 return ChannelTypeBuilder.state(channelTypeUID, field.getName(), itemType)//
153 .isAdvanced(advanced)//
154 .withDescription(field.getInformativeText())//
155 .withStateDescriptionFragment(stateDescBuilder.build()).build();
158 private static String getPattern(Field field) {
159 String format = getFormat(field);
160 String unit = getUnit(field);
161 StringBuilder pattern = new StringBuilder();
162 pattern.append(format);
164 pattern.append(" ").append(unit);
166 return pattern.toString();
169 private static List<StateOption> getStateOptions(Field field) {
170 return Optional.ofNullable(field.getEnumerations())//
171 .map(Enumerations::getEnumerations)//
173 .flatMap(List::stream)
174 .map(enumeration -> new StateOption(String.valueOf(enumeration.getKey()), enumeration.getValue()))
175 .collect(Collectors.toList());
178 private static @Nullable BigDecimal toBigDecimal(@Nullable Double value) {
179 return value != null ? BigDecimal.valueOf(value) : null;
182 private static String getFormat(Field field) {
183 String format = "%s";
184 Integer decimalExponent = field.getDecimalExponent();
185 if (field.getFormat().isReal() && decimalExponent != null && decimalExponent < 0) {
186 format = "%." + Math.abs(decimalExponent) + "f";
191 private static @Nullable String getUnit(Field field) {
192 String gattUnit = field.getUnit();
193 if (gattUnit != null) {
194 BluetoothUnit unit = BluetoothUnit.findByType(gattUnit);
196 return unit.getUnit().getSymbol();