]> git.basschouten.com Git - openhab-addons.git/blob
1e759fbb832330fb3d31d848ad0a5e9316e8ffc5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.core.thing.type.ChannelType;
29 import org.openhab.core.thing.type.ChannelTypeBuilder;
30 import org.openhab.core.thing.type.ChannelTypeProvider;
31 import org.openhab.core.thing.type.ChannelTypeUID;
32 import org.openhab.core.types.StateDescriptionFragmentBuilder;
33 import org.openhab.core.types.StateOption;
34 import org.osgi.service.component.annotations.Component;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.sputnikdev.bluetooth.gattparser.BluetoothGattParser;
38 import org.sputnikdev.bluetooth.gattparser.BluetoothGattParserFactory;
39 import org.sputnikdev.bluetooth.gattparser.spec.Enumerations;
40 import org.sputnikdev.bluetooth.gattparser.spec.Field;
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         if (channelID.charAt(67) != '-') {
112             return false;
113         }
114         return true;
115     }
116
117     public ChannelTypeUID registerChannelType(String characteristicUUID, boolean advanced, boolean readOnly,
118             Field field) {
119         // characteristic-advncd-readable-00002a04-0000-1000-8000-00805f9b34fb-Battery_Level
120         String channelType = String.format(CHANNEL_TYPE_NAME_PATTERN, advanced ? "advncd" : "simple",
121                 readOnly ? "readable" : "writable", characteristicUUID, BluetoothChannelUtils.encodeFieldID(field));
122
123         ChannelTypeUID channelTypeUID = new ChannelTypeUID(BluetoothBindingConstants.BINDING_ID, channelType);
124         cache.computeIfAbsent(channelTypeUID, uid -> buildChannelType(uid, advanced, readOnly, field));
125         logger.debug("registered channel type: {}", channelTypeUID);
126         return channelTypeUID;
127     }
128
129     private ChannelType buildChannelType(ChannelTypeUID channelTypeUID, boolean advanced, boolean readOnly,
130             Field field) {
131         List<StateOption> options = getStateOptions(field);
132         String itemType = BluetoothChannelUtils.getItemType(field);
133
134         if (itemType == null) {
135             throw new IllegalStateException("Unknown field format type: " + field.getUnit());
136         }
137
138         if (itemType.equals("Switch")) {
139             options = Collections.emptyList();
140         }
141
142         StateDescriptionFragmentBuilder stateDescBuilder = StateDescriptionFragmentBuilder.create()//
143                 .withPattern(getPattern(field))//
144                 .withReadOnly(readOnly)//
145                 .withOptions(options);
146
147         BigDecimal min = toBigDecimal(field.getMinimum());
148         BigDecimal max = toBigDecimal(field.getMaximum());
149         if (min != null) {
150             stateDescBuilder = stateDescBuilder.withMinimum(min);
151         }
152         if (max != null) {
153             stateDescBuilder = stateDescBuilder.withMaximum(max);
154         }
155         return ChannelTypeBuilder.state(channelTypeUID, field.getName(), itemType)//
156                 .isAdvanced(advanced)//
157                 .withDescription(field.getInformativeText())//
158                 .withStateDescriptionFragment(stateDescBuilder.build()).build();
159     }
160
161     private static String getPattern(Field field) {
162         String format = getFormat(field);
163         String unit = getUnit(field);
164         StringBuilder pattern = new StringBuilder();
165         pattern.append(format);
166         if (unit != null) {
167             pattern.append(" ").append(unit);
168         }
169         return pattern.toString();
170     }
171
172     private static List<StateOption> getStateOptions(Field field) {
173         return Optional.ofNullable(field.getEnumerations())//
174                 .map(Enumerations::getEnumerations)//
175                 .stream()//
176                 .flatMap(List::stream)
177                 .map(enumeration -> new StateOption(String.valueOf(enumeration.getKey()), enumeration.getValue()))
178                 .collect(Collectors.toList());
179     }
180
181     private static @Nullable BigDecimal toBigDecimal(@Nullable Double value) {
182         return value != null ? BigDecimal.valueOf(value) : null;
183     }
184
185     private static String getFormat(Field field) {
186         String format = "%s";
187         Integer decimalExponent = field.getDecimalExponent();
188         if (field.getFormat().isReal() && decimalExponent != null && decimalExponent < 0) {
189             format = "%." + Math.abs(decimalExponent) + "f";
190         }
191         return format;
192     }
193
194     private static @Nullable String getUnit(Field field) {
195         String gattUnit = field.getUnit();
196         if (gattUnit != null) {
197             BluetoothUnit unit = BluetoothUnit.findByType(gattUnit);
198             if (unit != null) {
199                 return unit.getUnit().getSymbol();
200             }
201         }
202         return null;
203     }
204 }