2 * Copyright (c) 2010-2022 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.homematic.internal.type;
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
16 import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
18 import java.math.BigDecimal;
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;
26 import java.util.Objects;
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.binding.homematic.internal.type.MetadataUtils.OptionsBuilder;
35 import org.openhab.core.config.core.ConfigDescriptionBuilder;
36 import org.openhab.core.config.core.ConfigDescriptionParameter;
37 import org.openhab.core.config.core.ConfigDescriptionParameterBuilder;
38 import org.openhab.core.config.core.ConfigDescriptionParameterGroup;
39 import org.openhab.core.config.core.ConfigDescriptionParameterGroupBuilder;
40 import org.openhab.core.config.core.ParameterOption;
41 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.type.ChannelDefinition;
45 import org.openhab.core.thing.type.ChannelDefinitionBuilder;
46 import org.openhab.core.thing.type.ChannelGroupDefinition;
47 import org.openhab.core.thing.type.ChannelGroupType;
48 import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
49 import org.openhab.core.thing.type.ChannelGroupTypeUID;
50 import org.openhab.core.thing.type.ChannelType;
51 import org.openhab.core.thing.type.ChannelTypeBuilder;
52 import org.openhab.core.thing.type.ChannelTypeUID;
53 import org.openhab.core.thing.type.ThingType;
54 import org.openhab.core.thing.type.ThingTypeBuilder;
55 import org.openhab.core.types.EventDescription;
56 import org.openhab.core.types.EventOption;
57 import org.openhab.core.types.StateDescriptionFragmentBuilder;
58 import org.openhab.core.types.StateOption;
59 import org.osgi.service.component.annotations.Activate;
60 import org.osgi.service.component.annotations.Component;
61 import org.osgi.service.component.annotations.Reference;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
66 * Generates ThingTypes based on metadata from a Homematic gateway.
68 * @author Gerhard Riegler - Initial contribution
71 public class HomematicTypeGeneratorImpl implements HomematicTypeGenerator {
72 private final Logger logger = LoggerFactory.getLogger(HomematicTypeGeneratorImpl.class);
73 private static URI configDescriptionUriChannel;
75 private HomematicThingTypeProvider thingTypeProvider;
76 private HomematicChannelTypeProvider channelTypeProvider;
77 private HomematicChannelGroupTypeProvider channelGroupTypeProvider;
78 private HomematicConfigDescriptionProvider configDescriptionProvider;
79 private final Map<String, Set<String>> firmwaresByType = new HashMap<>();
81 private static final String[] IGNORE_DATAPOINT_NAMES = new String[] { DATAPOINT_NAME_AES_KEY,
82 VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY };
84 public HomematicTypeGeneratorImpl() {
86 configDescriptionUriChannel = new URI(CONFIG_DESCRIPTION_URI_CHANNEL);
87 } catch (Exception ex) {
88 logger.warn("Can't create ConfigDescription URI '{}', ConfigDescription for channels not avilable!",
89 CONFIG_DESCRIPTION_URI_CHANNEL);
94 protected void setThingTypeProvider(HomematicThingTypeProvider thingTypeProvider) {
95 this.thingTypeProvider = thingTypeProvider;
98 protected void unsetThingTypeProvider(HomematicThingTypeProvider thingTypeProvider) {
99 this.thingTypeProvider = null;
103 protected void setChannelTypeProvider(HomematicChannelTypeProvider channelTypeProvider) {
104 this.channelTypeProvider = channelTypeProvider;
107 protected void unsetChannelTypeProvider(HomematicChannelTypeProvider channelTypeProvider) {
108 this.channelTypeProvider = null;
112 protected void setChannelGroupTypeProvider(HomematicChannelGroupTypeProvider channelGroupTypeProvider) {
113 this.channelGroupTypeProvider = channelGroupTypeProvider;
116 protected void unsetChannelGroupTypeProvider(HomematicChannelGroupTypeProvider channelGroupTypeProvider) {
117 this.channelGroupTypeProvider = null;
121 protected void setConfigDescriptionProvider(HomematicConfigDescriptionProvider configDescriptionProvider) {
122 this.configDescriptionProvider = configDescriptionProvider;
125 protected void unsetConfigDescriptionProvider(HomematicConfigDescriptionProvider configDescriptionProvider) {
126 this.configDescriptionProvider = null;
131 public void initialize() {
132 MetadataUtils.initialize();
136 public void generate(HmDevice device) {
137 if (thingTypeProvider != null) {
138 ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
139 ThingType tt = thingTypeProvider.getInternalThingType(thingTypeUID);
141 if (tt == null || device.isGatewayExtras()) {
142 logger.debug("Generating ThingType for device '{}' with {} datapoints", device.getType(),
143 device.getDatapointCount());
145 List<ChannelGroupType> groupTypes = new ArrayList<>();
146 for (HmChannel channel : device.getChannels()) {
147 List<ChannelDefinition> channelDefinitions = new ArrayList<>();
148 // Omit thing channel definitions for reconfigurable channels;
149 // those will be populated dynamically during thing initialization
150 if (!channel.isReconfigurable()) {
152 for (HmDatapoint dp : channel.getDatapoints()) {
153 if (!isIgnoredDatapoint(dp) && dp.getParamsetType() == HmParamsetType.VALUES) {
154 ChannelTypeUID channelTypeUID = UidUtils.generateChannelTypeUID(dp);
155 ChannelType channelType = channelTypeProvider.getInternalChannelType(channelTypeUID);
156 if (channelType == null) {
157 channelType = createChannelType(dp, channelTypeUID);
158 channelTypeProvider.addChannelType(channelType);
161 ChannelDefinition channelDef = new ChannelDefinitionBuilder(dp.getName(),
162 channelType.getUID()).build();
163 channelDefinitions.add(channelDef);
169 ChannelGroupTypeUID groupTypeUID = UidUtils.generateChannelGroupTypeUID(channel);
170 ChannelGroupType groupType = channelGroupTypeProvider.getInternalChannelGroupType(groupTypeUID);
171 if (groupType == null || device.isGatewayExtras()) {
172 String groupLabel = String.format("%s", channel.getType() == null ? null
173 : MiscUtils.capitalize(channel.getType().replace("_", " ")));
174 groupType = ChannelGroupTypeBuilder.instance(groupTypeUID, groupLabel)
175 .withChannelDefinitions(channelDefinitions).build();
176 channelGroupTypeProvider.addChannelGroupType(groupType);
177 groupTypes.add(groupType);
181 tt = createThingType(device, groupTypes);
182 thingTypeProvider.addThingType(tt);
189 public void validateFirmwares() {
190 for (String deviceType : firmwaresByType.keySet()) {
191 Set<String> firmwares = firmwaresByType.get(deviceType);
192 if (firmwares.size() > 1) {
194 "Multiple firmware versions for device type '{}' found ({}). "
195 + "Make sure, all devices of the same type have the same firmware version, "
196 + "otherwise you MAY have channel and/or datapoint errors in the logfile",
197 deviceType, String.join(", ", firmwares));
203 * Adds the firmware version for validation.
205 private void addFirmware(HmDevice device) {
206 if (!"?".equals(device.getFirmware()) && !DEVICE_TYPE_VIRTUAL.equals(device.getType())
207 && !DEVICE_TYPE_VIRTUAL_WIRED.equals(device.getType())) {
208 Set<String> firmwares = firmwaresByType.get(device.getType());
209 if (firmwares == null) {
210 firmwares = new HashSet<>();
211 firmwaresByType.put(device.getType(), firmwares);
213 firmwares.add(device.getFirmware());
218 * Creates the ThingType for the given device.
220 private ThingType createThingType(HmDevice device, List<ChannelGroupType> groupTypes) {
221 String label = MetadataUtils.getDeviceName(device);
222 String description = String.format("%s (%s)", label, device.getType());
224 List<String> supportedBridgeTypeUids = new ArrayList<>();
225 supportedBridgeTypeUids.add(THING_TYPE_BRIDGE.toString());
226 ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
228 Map<String, String> properties = new HashMap<>();
229 properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
230 properties.put(Thing.PROPERTY_MODEL_ID, device.getType());
232 URI configDescriptionURI = getConfigDescriptionURI(device);
233 if (configDescriptionProvider.getInternalConfigDescription(configDescriptionURI) == null) {
234 generateConfigDescription(device, configDescriptionURI);
237 List<ChannelGroupDefinition> groupDefinitions = new ArrayList<>();
238 for (ChannelGroupType groupType : groupTypes) {
239 int usPos = groupType.getUID().getId().lastIndexOf("_");
240 String id = usPos == -1 ? groupType.getUID().getId() : groupType.getUID().getId().substring(usPos + 1);
241 groupDefinitions.add(new ChannelGroupDefinition(id, groupType.getUID()));
244 return ThingTypeBuilder.instance(thingTypeUID, label).withSupportedBridgeTypeUIDs(supportedBridgeTypeUids)
245 .withDescription(description).withChannelGroupDefinitions(groupDefinitions).withProperties(properties)
246 .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).withConfigDescriptionURI(configDescriptionURI)
251 * Creates the ChannelType for the given datapoint.
253 public static 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;
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);
267 List<StateOption> options = null;
268 if (dp.isEnumType()) {
269 options = MetadataUtils.generateOptions(dp, new OptionsBuilder<StateOption>() {
271 public StateOption createOption(String value, String description) {
272 return new StateOption(value, description);
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);
291 .createBigDecimal(dp.isFloatType() ? Float.valueOf(0.1f) : Long.valueOf(1L));
294 stateFragment.withMinimum(min).withMaximum(max).withStep(step)
295 .withPattern(MetadataUtils.getStatePattern(dp)).withReadOnly(dp.isReadOnly());
297 stateFragment.withPattern(MetadataUtils.getStatePattern(dp)).withReadOnly(dp.isReadOnly());
299 if (options != null) {
300 stateFragment.withOptions(options);
303 ChannelTypeBuilder channelTypeBuilder;
304 EventDescription eventDescription = null;
305 if (dp.isTrigger()) {
306 eventDescription = new EventDescription(
307 MetadataUtils.generateOptions(dp, new OptionsBuilder<EventOption>() {
309 public EventOption createOption(String value, String description) {
310 return new EventOption(value, description);
313 channelTypeBuilder = ChannelTypeBuilder.trigger(channelTypeUID, label)
314 .withEventDescription(eventDescription);
316 channelTypeBuilder = ChannelTypeBuilder.state(channelTypeUID, label, itemType)
317 .withStateDescriptionFragment(stateFragment.build());
319 channelType = channelTypeBuilder.isAdvanced(!MetadataUtils.isStandard(dp)).withDescription(description)
320 .withCategory(category).withConfigDescriptionURI(configDescriptionUriChannel).build();
325 private void generateConfigDescription(HmDevice device, URI configDescriptionURI) {
326 List<ConfigDescriptionParameter> parms = new ArrayList<>();
327 List<ConfigDescriptionParameterGroup> groups = new ArrayList<>();
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());
334 for (HmDatapoint dp : channel.getDatapoints()) {
335 if (dp.getParamsetType() == HmParamsetType.MASTER) {
336 ConfigDescriptionParameterBuilder builder = ConfigDescriptionParameterBuilder.create(
337 MetadataUtils.getParameterName(dp), MetadataUtils.getConfigDescriptionParameterType(dp));
339 builder.withLabel(MetadataUtils.getLabel(dp));
340 builder.withDefault(Objects.toString(dp.getDefaultValue(), ""));
341 builder.withDescription(MetadataUtils.getDatapointDescription(dp));
342 if (dp.isEnumType()) {
343 builder.withLimitToOptions(dp.isEnumType());
344 List<ParameterOption> options = MetadataUtils.generateOptions(dp,
345 new OptionsBuilder<ParameterOption>() {
347 public ParameterOption createOption(String value, String description) {
348 return new ParameterOption(value, description);
351 builder.withOptions(options);
354 if (dp.isNumberType()) {
355 Number defaultValue = (Number) dp.getDefaultValue();
356 Number maxValue = dp.getMaxValue();
357 // some datapoints can have a default value that is greater than the maximum value
358 if (defaultValue != null && maxValue != null
359 && defaultValue.doubleValue() > maxValue.doubleValue()) {
360 maxValue = defaultValue;
362 builder.withMinimum(MetadataUtils.createBigDecimal(dp.getMinValue()));
363 builder.withMaximum(MetadataUtils.createBigDecimal(maxValue));
364 builder.withStepSize(MetadataUtils
365 .createBigDecimal(dp.isFloatType() ? Float.valueOf(0.1f) : Long.valueOf(1L)));
366 builder.withUnitLabel(MetadataUtils.getUnit(dp));
369 builder.withGroupName(groupName);
370 parms.add(builder.build());
374 configDescriptionProvider.addConfigDescription(ConfigDescriptionBuilder.create(configDescriptionURI)
375 .withParameters(parms).withParameterGroups(groups).build());
378 private URI getConfigDescriptionURI(HmDevice device) {
381 String.format("%s:%s", CONFIG_DESCRIPTION_URI_THING_PREFIX, UidUtils.generateThingTypeUID(device)));
382 } catch (URISyntaxException ex) {
383 logger.warn("Can't create configDescriptionURI for device type {}", device.getType());
389 * Returns true, if the given datapoint can be ignored for metadata generation.
391 public static boolean isIgnoredDatapoint(HmDatapoint dp) {
392 for (String testValue : IGNORE_DATAPOINT_NAMES) {
393 if (dp.getName().indexOf(testValue) > -1) {