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