]> git.basschouten.com Git - openhab-addons.git/blob
a088fc5f6d15d3f975b5d120294a77be6015433d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.homematic.internal.type;
14
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
16 import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
17
18 import java.math.BigDecimal;
19 import java.net.URI;
20 import java.net.URISyntaxException;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.Set;
28
29 import org.openhab.binding.homematic.internal.misc.MiscUtils;
30 import org.openhab.binding.homematic.internal.model.HmChannel;
31 import org.openhab.binding.homematic.internal.model.HmDatapoint;
32 import org.openhab.binding.homematic.internal.model.HmDevice;
33 import org.openhab.binding.homematic.internal.model.HmParamsetType;
34 import org.openhab.core.config.core.ConfigDescriptionBuilder;
35 import org.openhab.core.config.core.ConfigDescriptionParameter;
36 import org.openhab.core.config.core.ConfigDescriptionParameterBuilder;
37 import org.openhab.core.config.core.ConfigDescriptionParameterGroup;
38 import org.openhab.core.config.core.ConfigDescriptionParameterGroupBuilder;
39 import org.openhab.core.config.core.ParameterOption;
40 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.thing.type.ChannelDefinition;
44 import org.openhab.core.thing.type.ChannelDefinitionBuilder;
45 import org.openhab.core.thing.type.ChannelGroupDefinition;
46 import org.openhab.core.thing.type.ChannelGroupType;
47 import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
48 import org.openhab.core.thing.type.ChannelGroupTypeUID;
49 import org.openhab.core.thing.type.ChannelType;
50 import org.openhab.core.thing.type.ChannelTypeBuilder;
51 import org.openhab.core.thing.type.ChannelTypeUID;
52 import org.openhab.core.thing.type.ThingType;
53 import org.openhab.core.thing.type.ThingTypeBuilder;
54 import org.openhab.core.types.EventDescription;
55 import org.openhab.core.types.EventOption;
56 import org.openhab.core.types.StateDescriptionFragmentBuilder;
57 import org.openhab.core.types.StateOption;
58 import org.osgi.service.component.annotations.Activate;
59 import org.osgi.service.component.annotations.Component;
60 import org.osgi.service.component.annotations.Reference;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 /**
65  * Generates ThingTypes based on metadata from a Homematic gateway.
66  *
67  * @author Gerhard Riegler - Initial contribution
68  */
69 @Component
70 public class HomematicTypeGeneratorImpl implements HomematicTypeGenerator {
71     private final Logger logger = LoggerFactory.getLogger(HomematicTypeGeneratorImpl.class);
72     private static URI configDescriptionUriChannel;
73
74     private HomematicThingTypeProvider thingTypeProvider;
75     private HomematicChannelTypeProvider channelTypeProvider;
76     private HomematicChannelGroupTypeProvider channelGroupTypeProvider;
77     private HomematicConfigDescriptionProvider configDescriptionProvider;
78     private final Map<String, Set<String>> firmwaresByType = new HashMap<>();
79
80     private static final String[] IGNORE_DATAPOINT_NAMES = new String[] { DATAPOINT_NAME_AES_KEY,
81             VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY };
82
83     public HomematicTypeGeneratorImpl() {
84         try {
85             configDescriptionUriChannel = new URI(CONFIG_DESCRIPTION_URI_CHANNEL);
86         } catch (Exception ex) {
87             logger.warn("Can't create ConfigDescription URI '{}', ConfigDescription for channels not avilable!",
88                     CONFIG_DESCRIPTION_URI_CHANNEL);
89         }
90     }
91
92     @Reference
93     protected void setThingTypeProvider(HomematicThingTypeProvider thingTypeProvider) {
94         this.thingTypeProvider = thingTypeProvider;
95     }
96
97     protected void unsetThingTypeProvider(HomematicThingTypeProvider thingTypeProvider) {
98         this.thingTypeProvider = null;
99     }
100
101     @Reference
102     protected void setChannelTypeProvider(HomematicChannelTypeProvider channelTypeProvider) {
103         this.channelTypeProvider = channelTypeProvider;
104     }
105
106     protected void unsetChannelTypeProvider(HomematicChannelTypeProvider channelTypeProvider) {
107         this.channelTypeProvider = null;
108     }
109
110     @Reference
111     protected void setChannelGroupTypeProvider(HomematicChannelGroupTypeProvider channelGroupTypeProvider) {
112         this.channelGroupTypeProvider = channelGroupTypeProvider;
113     }
114
115     protected void unsetChannelGroupTypeProvider(HomematicChannelGroupTypeProvider channelGroupTypeProvider) {
116         this.channelGroupTypeProvider = null;
117     }
118
119     @Reference
120     protected void setConfigDescriptionProvider(HomematicConfigDescriptionProvider configDescriptionProvider) {
121         this.configDescriptionProvider = configDescriptionProvider;
122     }
123
124     protected void unsetConfigDescriptionProvider(HomematicConfigDescriptionProvider configDescriptionProvider) {
125         this.configDescriptionProvider = null;
126     }
127
128     @Override
129     @Activate
130     public void initialize() {
131         MetadataUtils.initialize();
132     }
133
134     @Override
135     public void generate(HmDevice device) {
136         if (thingTypeProvider != null) {
137             ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
138             ThingType tt = thingTypeProvider.getInternalThingType(thingTypeUID);
139
140             if (tt == null || device.isGatewayExtras()) {
141                 logger.debug("Generating ThingType for device '{}' with {} datapoints", device.getType(),
142                         device.getDatapointCount());
143
144                 List<ChannelGroupType> groupTypes = new ArrayList<>();
145                 for (HmChannel channel : device.getChannels()) {
146                     List<ChannelDefinition> channelDefinitions = new ArrayList<>();
147                     // Omit thing channel definitions for reconfigurable channels;
148                     // those will be populated dynamically during thing initialization
149                     if (!channel.isReconfigurable()) {
150                         // generate channel
151                         for (HmDatapoint dp : channel.getDatapoints()) {
152                             if (!isIgnoredDatapoint(dp) && dp.getParamsetType() == HmParamsetType.VALUES) {
153                                 ChannelTypeUID channelTypeUID = UidUtils.generateChannelTypeUID(dp);
154                                 ChannelType channelType = channelTypeProvider.getInternalChannelType(channelTypeUID);
155                                 if (channelType == null) {
156                                     channelType = createChannelType(dp, channelTypeUID);
157                                     channelTypeProvider.addChannelType(channelType);
158                                 }
159
160                                 ChannelDefinition channelDef = new ChannelDefinitionBuilder(dp.getName(),
161                                         channelType.getUID()).build();
162                                 channelDefinitions.add(channelDef);
163                             }
164                         }
165                     }
166
167                     // generate group
168                     ChannelGroupTypeUID groupTypeUID = UidUtils.generateChannelGroupTypeUID(channel);
169                     ChannelGroupType groupType = channelGroupTypeProvider.getInternalChannelGroupType(groupTypeUID);
170                     if (groupType == null || device.isGatewayExtras()) {
171                         String groupLabel = String.format("%s", channel.getType() == null ? null
172                                 : MiscUtils.capitalize(channel.getType().replace("_", " ")));
173                         groupType = ChannelGroupTypeBuilder.instance(groupTypeUID, groupLabel)
174                                 .withChannelDefinitions(channelDefinitions).build();
175                         channelGroupTypeProvider.addChannelGroupType(groupType);
176                         groupTypes.add(groupType);
177                     }
178
179                 }
180                 tt = createThingType(device, groupTypes);
181                 thingTypeProvider.addThingType(tt);
182             }
183             addFirmware(device);
184         }
185     }
186
187     @Override
188     public void validateFirmwares() {
189         for (String deviceType : firmwaresByType.keySet()) {
190             Set<String> firmwares = firmwaresByType.get(deviceType);
191             if (firmwares.size() > 1) {
192                 logger.info("""
193                         Multiple firmware versions for device type '{}' found ({}). \
194                         Make sure, all devices of the same type have the same firmware version, \
195                         otherwise you MAY have channel and/or datapoint errors in the logfile\
196                         """, deviceType, String.join(", ", firmwares));
197             }
198         }
199     }
200
201     /**
202      * Adds the firmware version for validation.
203      */
204     private void addFirmware(HmDevice device) {
205         if (!"?".equals(device.getFirmware()) && !DEVICE_TYPE_VIRTUAL.equals(device.getType())
206                 && !DEVICE_TYPE_VIRTUAL_WIRED.equals(device.getType())) {
207             Set<String> firmwares = firmwaresByType.get(device.getType());
208             if (firmwares == null) {
209                 firmwares = new HashSet<>();
210                 firmwaresByType.put(device.getType(), firmwares);
211             }
212             firmwares.add(device.getFirmware());
213         }
214     }
215
216     /**
217      * Creates the ThingType for the given device.
218      */
219     private ThingType createThingType(HmDevice device, List<ChannelGroupType> groupTypes) {
220         String label = MetadataUtils.getDeviceName(device);
221         String description = String.format("%s (%s)", label, device.getType());
222
223         List<String> supportedBridgeTypeUids = new ArrayList<>();
224         supportedBridgeTypeUids.add(THING_TYPE_BRIDGE.toString());
225         ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
226
227         Map<String, String> properties = new HashMap<>();
228         properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
229         properties.put(Thing.PROPERTY_MODEL_ID, device.getType());
230
231         URI configDescriptionURI = getConfigDescriptionURI(device);
232         if (configDescriptionProvider.getInternalConfigDescription(configDescriptionURI) == null) {
233             generateConfigDescription(device, configDescriptionURI);
234         }
235
236         List<ChannelGroupDefinition> groupDefinitions = new ArrayList<>();
237         for (ChannelGroupType groupType : groupTypes) {
238             int usPos = groupType.getUID().getId().lastIndexOf("_");
239             String id = usPos == -1 ? groupType.getUID().getId() : groupType.getUID().getId().substring(usPos + 1);
240             groupDefinitions.add(new ChannelGroupDefinition(id, groupType.getUID()));
241         }
242
243         return ThingTypeBuilder.instance(thingTypeUID, label).withSupportedBridgeTypeUIDs(supportedBridgeTypeUids)
244                 .withDescription(description).withChannelGroupDefinitions(groupDefinitions).withProperties(properties)
245                 .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).withConfigDescriptionURI(configDescriptionURI)
246                 .build();
247     }
248
249     /**
250      * Creates the ChannelType for the given datapoint.
251      */
252     public static ChannelType createChannelType(HmDatapoint dp, ChannelTypeUID channelTypeUID) {
253         ChannelType channelType;
254         if (dp.getName().equals(DATAPOINT_NAME_LOWBAT) || dp.getName().equals(DATAPOINT_NAME_LOWBAT_IP)) {
255             channelType = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_LOW_BATTERY;
256         } else if (dp.getName().equals(VIRTUAL_DATAPOINT_NAME_SIGNAL_STRENGTH)) {
257             channelType = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_SIGNAL_STRENGTH;
258         } else if (dp.getName().equals(VIRTUAL_DATAPOINT_NAME_BUTTON)) {
259             channelType = DefaultSystemChannelTypeProvider.SYSTEM_BUTTON;
260         } else {
261             String itemType = MetadataUtils.getItemType(dp);
262             StateDescriptionFragmentBuilder stateFragment = StateDescriptionFragmentBuilder.create()
263                     .withPattern(MetadataUtils.getStatePattern(dp)).withReadOnly(dp.isReadOnly());
264
265             if (dp.isNumberType()) {
266                 final BigDecimal min, max;
267                 if (ITEM_TYPE_DIMMER.equals(itemType) || ITEM_TYPE_ROLLERSHUTTER.equals(itemType)) {
268                     // those types use PercentTypeConverter, so set up min and max as percent values
269                     min = MetadataUtils.createBigDecimal(0);
270                     max = MetadataUtils.createBigDecimal(100);
271                 } else {
272                     min = MetadataUtils.createBigDecimal(dp.getMinValue());
273                     max = MetadataUtils.createBigDecimal(dp.getMaxValue());
274                 }
275                 stateFragment.withMinimum(min).withMaximum(max);
276             } else if (dp.isEnumType()) {
277                 List<StateOption> options = MetadataUtils.generateOptions(dp,
278                         (value, description) -> new StateOption(value, description));
279                 if (options != null) {
280                     stateFragment.withOptions(options);
281                 }
282             }
283
284             String label = MetadataUtils.getLabel(dp);
285             final ChannelTypeBuilder channelTypeBuilder;
286             if (dp.isTrigger()) {
287                 EventDescription eventDescription = new EventDescription(
288                         MetadataUtils.generateOptions(dp, (value, description) -> new EventOption(value, description)));
289                 channelTypeBuilder = ChannelTypeBuilder.trigger(channelTypeUID, label)
290                         .withEventDescription(eventDescription);
291             } else {
292                 channelTypeBuilder = ChannelTypeBuilder.state(channelTypeUID, label, itemType)
293                         .withStateDescriptionFragment(stateFragment.build());
294             }
295             channelType = channelTypeBuilder.isAdvanced(!MetadataUtils.isStandard(dp))
296                     .withDescription(MetadataUtils.getDatapointDescription(dp))
297                     .withCategory(MetadataUtils.getCategory(dp, itemType))
298                     .withConfigDescriptionURI(configDescriptionUriChannel).build();
299         }
300         return channelType;
301     }
302
303     private void generateConfigDescription(HmDevice device, URI configDescriptionURI) {
304         List<ConfigDescriptionParameter> parms = new ArrayList<>();
305         List<ConfigDescriptionParameterGroup> groups = new ArrayList<>();
306
307         for (HmChannel channel : device.getChannels()) {
308             String groupName = "HMG_" + channel.getNumber();
309             String groupLabel = MetadataUtils.getDescription("CHANNEL_NAME") + " " + channel.getNumber();
310             groups.add(ConfigDescriptionParameterGroupBuilder.create(groupName).withLabel(groupLabel).build());
311
312             for (HmDatapoint dp : channel.getDatapoints()) {
313                 if (dp.getParamsetType() == HmParamsetType.MASTER) {
314                     String defaultValueString = Objects.toString(dp.getDefaultValue(), "");
315                     ConfigDescriptionParameterBuilder builder = ConfigDescriptionParameterBuilder.create(
316                             MetadataUtils.getParameterName(dp), MetadataUtils.getConfigDescriptionParameterType(dp));
317
318                     builder.withLabel(MetadataUtils.getLabel(dp));
319                     builder.withDefault(defaultValueString);
320                     builder.withDescription(MetadataUtils.getDatapointDescription(dp));
321                     if (dp.isEnumType()) {
322                         builder.withLimitToOptions(dp.isEnumType());
323                         List<ParameterOption> options = MetadataUtils.generateOptions(dp,
324                                 (value, description) -> new ParameterOption(value, description));
325                         builder.withOptions(options);
326                         if (dp.isEnumType()) {
327                             logger.trace("Checking if default option {} is valid",
328                                     Objects.toString(dp.getDefaultValue(), ""));
329                             boolean needsChange = options.stream()
330                                     .noneMatch(opt -> opt.getValue().equals(defaultValueString));
331                             if (needsChange) {
332                                 String defStr = Objects.toString(dp.getDefaultValue(), "0");
333                                 int offset = defStr != null ? Integer.valueOf(defStr) : 0;
334                                 if (offset >= 0 && offset < options.size()) {
335                                     ParameterOption defaultOption = options.get(offset);
336                                     logger.trace("Changing default option to {} (offset {})", defaultOption, offset);
337                                     builder.withDefault(defaultOption.getValue());
338                                 } else if (!options.isEmpty()) {
339                                     ParameterOption defaultOption = options.get(0);
340                                     logger.trace("Changing default option to {} (first value)", defaultOption);
341                                     builder.withDefault(defaultOption.getValue());
342                                 }
343                             }
344                         }
345                     }
346
347                     if (dp.isNumberType()) {
348                         Number defaultValue = (Number) dp.getDefaultValue();
349                         Number maxValue = dp.getMaxValue();
350                         Number minValue = dp.getMinValue();
351                         if (defaultValue != null) {
352                             // some datapoints can have a default value that is greater than the maximum value
353                             if (maxValue != null && defaultValue.doubleValue() > maxValue.doubleValue()) {
354                                 maxValue = defaultValue;
355                             }
356                             // ... and there are also default values less than the minimum value
357                             if (minValue != null && defaultValue.doubleValue() < minValue.doubleValue()) {
358                                 minValue = defaultValue;
359                             }
360                         }
361                         builder.withMinimum(MetadataUtils.createBigDecimal(minValue));
362                         builder.withMaximum(MetadataUtils.createBigDecimal(maxValue));
363                         builder.withUnitLabel(MetadataUtils.getUnit(dp));
364                     }
365
366                     builder.withGroupName(groupName);
367                     parms.add(builder.build());
368                 }
369             }
370         }
371         configDescriptionProvider.addConfigDescription(ConfigDescriptionBuilder.create(configDescriptionURI)
372                 .withParameters(parms).withParameterGroups(groups).build());
373     }
374
375     private URI getConfigDescriptionURI(HmDevice device) {
376         try {
377             return new URI(
378                     String.format("%s:%s", CONFIG_DESCRIPTION_URI_THING_PREFIX, UidUtils.generateThingTypeUID(device)));
379         } catch (URISyntaxException ex) {
380             logger.warn("Can't create configDescriptionURI for device type {}", device.getType());
381             return null;
382         }
383     }
384
385     /**
386      * Returns true, if the given datapoint can be ignored for metadata generation.
387      */
388     public static boolean isIgnoredDatapoint(HmDatapoint dp) {
389         for (String testValue : IGNORE_DATAPOINT_NAMES) {
390             if (dp.getName().contains(testValue)) {
391                 return true;
392             }
393         }
394         return false;
395     }
396 }