]> git.basschouten.com Git - openhab-addons.git/blob
dd20532b4d35cc5cdd825e49acbbbe6bd8c879de
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.bluetooth.generic.internal;
14
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;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.stream.Collectors;
24
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;
41
42 /**
43  * {@link CharacteristicChannelTypeProvider} that provides channel types for dynamically discovered characteristics.
44  *
45  * @author Vlad Kolotov - Original author
46  * @author Connor Petty - Modified for openHAB use.
47  */
48 @NonNullByDefault
49 @Component(service = { CharacteristicChannelTypeProvider.class, ChannelTypeProvider.class })
50 public class CharacteristicChannelTypeProvider implements ChannelTypeProvider {
51
52     private static final String CHANNEL_TYPE_NAME_PATTERN = "characteristic-%s-%s-%s-%s";
53
54     private final Logger logger = LoggerFactory.getLogger(CharacteristicChannelTypeProvider.class);
55
56     private final @NonNullByDefault({}) Map<ChannelTypeUID, ChannelType> cache = new ConcurrentHashMap<>();
57
58     private final BluetoothGattParser gattParser = BluetoothGattParserFactory.getDefault();
59
60     @Override
61     public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
62         return cache.values();
63     }
64
65     @Override
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());
74
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());
79
80                     if (fields.size() > 1) {
81                         logger.warn("Multiple fields with the same name found: {} / {}. Skipping them.",
82                                 characteristicUUID, fieldName);
83                         return null;
84                     }
85                     Field field = fields.get(0);
86                     return buildChannelType(uid, advanced, readOnly, field);
87                 }
88                 return null;
89             });
90         }
91         return null;
92     }
93
94     private static boolean isValidUID(ChannelTypeUID channelTypeUID) {
95         if (!channelTypeUID.getBindingId().equals(BluetoothBindingConstants.BINDING_ID)) {
96             return false;
97         }
98         String channelID = channelTypeUID.getId();
99         if (!channelID.startsWith("characteristic")) {
100             return false;
101         }
102         if (channelID.length() < 68) {
103             return false;
104         }
105         if (channelID.charAt(21) != '-') {
106             return false;
107         }
108         if (channelID.charAt(30) != '-') {
109             return false;
110         }
111         return channelID.charAt(67) == '-';
112     }
113
114     public ChannelTypeUID registerChannelType(String characteristicUUID, boolean advanced, boolean readOnly,
115             Field field) {
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));
119
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;
124     }
125
126     private ChannelType buildChannelType(ChannelTypeUID channelTypeUID, boolean advanced, boolean readOnly,
127             Field field) {
128         List<StateOption> options = getStateOptions(field);
129         String itemType = BluetoothChannelUtils.getItemType(field);
130
131         if (itemType == null) {
132             throw new IllegalStateException("Unknown field format type: " + field.getUnit());
133         }
134
135         if ("Switch".equals(itemType)) {
136             options = Collections.emptyList();
137         }
138
139         StateDescriptionFragmentBuilder stateDescBuilder = StateDescriptionFragmentBuilder.create()//
140                 .withPattern(getPattern(field))//
141                 .withReadOnly(readOnly)//
142                 .withOptions(options);
143
144         BigDecimal min = toBigDecimal(field.getMinimum());
145         BigDecimal max = toBigDecimal(field.getMaximum());
146         if (min != null) {
147             stateDescBuilder = stateDescBuilder.withMinimum(min);
148         }
149         if (max != null) {
150             stateDescBuilder = stateDescBuilder.withMaximum(max);
151         }
152         return ChannelTypeBuilder.state(channelTypeUID, field.getName(), itemType)//
153                 .isAdvanced(advanced)//
154                 .withDescription(field.getInformativeText())//
155                 .withStateDescriptionFragment(stateDescBuilder.build()).build();
156     }
157
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);
163         if (unit != null) {
164             pattern.append(" ").append(unit);
165         }
166         return pattern.toString();
167     }
168
169     private static List<StateOption> getStateOptions(Field field) {
170         return Optional.ofNullable(field.getEnumerations())//
171                 .map(Enumerations::getEnumerations)//
172                 .stream()//
173                 .flatMap(List::stream)
174                 .map(enumeration -> new StateOption(String.valueOf(enumeration.getKey()), enumeration.getValue()))
175                 .collect(Collectors.toList());
176     }
177
178     private static @Nullable BigDecimal toBigDecimal(@Nullable Double value) {
179         return value != null ? BigDecimal.valueOf(value) : null;
180     }
181
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";
187         }
188         return format;
189     }
190
191     private static @Nullable String getUnit(Field field) {
192         String gattUnit = field.getUnit();
193         if (gattUnit != null) {
194             BluetoothUnit unit = BluetoothUnit.findByType(gattUnit);
195             if (unit != null) {
196                 return unit.getUnit().getSymbol();
197             }
198         }
199         return null;
200     }
201 }