2 * Copyright (c) 2010-2023 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.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;
65 * Generates ThingTypes based on metadata from a Homematic gateway.
67 * @author Gerhard Riegler - Initial contribution
70 public class HomematicTypeGeneratorImpl implements HomematicTypeGenerator {
71 private final Logger logger = LoggerFactory.getLogger(HomematicTypeGeneratorImpl.class);
72 private static URI configDescriptionUriChannel;
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<>();
80 private static final String[] IGNORE_DATAPOINT_NAMES = new String[] { DATAPOINT_NAME_AES_KEY,
81 VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY };
83 public HomematicTypeGeneratorImpl() {
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);
93 protected void setThingTypeProvider(HomematicThingTypeProvider thingTypeProvider) {
94 this.thingTypeProvider = thingTypeProvider;
97 protected void unsetThingTypeProvider(HomematicThingTypeProvider thingTypeProvider) {
98 this.thingTypeProvider = null;
102 protected void setChannelTypeProvider(HomematicChannelTypeProvider channelTypeProvider) {
103 this.channelTypeProvider = channelTypeProvider;
106 protected void unsetChannelTypeProvider(HomematicChannelTypeProvider channelTypeProvider) {
107 this.channelTypeProvider = null;
111 protected void setChannelGroupTypeProvider(HomematicChannelGroupTypeProvider channelGroupTypeProvider) {
112 this.channelGroupTypeProvider = channelGroupTypeProvider;
115 protected void unsetChannelGroupTypeProvider(HomematicChannelGroupTypeProvider channelGroupTypeProvider) {
116 this.channelGroupTypeProvider = null;
120 protected void setConfigDescriptionProvider(HomematicConfigDescriptionProvider configDescriptionProvider) {
121 this.configDescriptionProvider = configDescriptionProvider;
124 protected void unsetConfigDescriptionProvider(HomematicConfigDescriptionProvider configDescriptionProvider) {
125 this.configDescriptionProvider = null;
130 public void initialize() {
131 MetadataUtils.initialize();
135 public void generate(HmDevice device) {
136 if (thingTypeProvider != null) {
137 ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
138 ThingType tt = thingTypeProvider.getInternalThingType(thingTypeUID);
140 if (tt == null || device.isGatewayExtras()) {
141 logger.debug("Generating ThingType for device '{}' with {} datapoints", device.getType(),
142 device.getDatapointCount());
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()) {
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);
160 ChannelDefinition channelDef = new ChannelDefinitionBuilder(dp.getName(),
161 channelType.getUID()).build();
162 channelDefinitions.add(channelDef);
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);
180 tt = createThingType(device, groupTypes);
181 thingTypeProvider.addThingType(tt);
188 public void validateFirmwares() {
189 for (String deviceType : firmwaresByType.keySet()) {
190 Set<String> firmwares = firmwaresByType.get(deviceType);
191 if (firmwares.size() > 1) {
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));
202 * Adds the firmware version for validation.
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);
212 firmwares.add(device.getFirmware());
217 * Creates the ThingType for the given device.
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());
223 List<String> supportedBridgeTypeUids = new ArrayList<>();
224 supportedBridgeTypeUids.add(THING_TYPE_BRIDGE.toString());
225 ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
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());
231 URI configDescriptionURI = getConfigDescriptionURI(device);
232 if (configDescriptionProvider.getInternalConfigDescription(configDescriptionURI) == null) {
233 generateConfigDescription(device, configDescriptionURI);
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()));
243 return ThingTypeBuilder.instance(thingTypeUID, label).withSupportedBridgeTypeUIDs(supportedBridgeTypeUids)
244 .withDescription(description).withChannelGroupDefinitions(groupDefinitions).withProperties(properties)
245 .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).withConfigDescriptionURI(configDescriptionURI)
250 * Creates the ChannelType for the given datapoint.
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;
261 String itemType = MetadataUtils.getItemType(dp);
262 StateDescriptionFragmentBuilder stateFragment = StateDescriptionFragmentBuilder.create()
263 .withPattern(MetadataUtils.getStatePattern(dp)).withReadOnly(dp.isReadOnly());
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);
272 min = MetadataUtils.createBigDecimal(dp.getMinValue());
273 max = MetadataUtils.createBigDecimal(dp.getMaxValue());
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);
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);
292 channelTypeBuilder = ChannelTypeBuilder.state(channelTypeUID, label, itemType)
293 .withStateDescriptionFragment(stateFragment.build());
295 channelType = channelTypeBuilder.isAdvanced(!MetadataUtils.isStandard(dp))
296 .withDescription(MetadataUtils.getDatapointDescription(dp))
297 .withCategory(MetadataUtils.getCategory(dp, itemType))
298 .withConfigDescriptionURI(configDescriptionUriChannel).build();
303 private void generateConfigDescription(HmDevice device, URI configDescriptionURI) {
304 List<ConfigDescriptionParameter> parms = new ArrayList<>();
305 List<ConfigDescriptionParameterGroup> groups = new ArrayList<>();
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());
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));
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));
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());
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;
356 // ... and there are also default values less than the minimum value
357 if (minValue != null && defaultValue.doubleValue() < minValue.doubleValue()) {
358 minValue = defaultValue;
361 builder.withMinimum(MetadataUtils.createBigDecimal(minValue));
362 builder.withMaximum(MetadataUtils.createBigDecimal(maxValue));
363 builder.withUnitLabel(MetadataUtils.getUnit(dp));
366 builder.withGroupName(groupName);
367 parms.add(builder.build());
371 configDescriptionProvider.addConfigDescription(ConfigDescriptionBuilder.create(configDescriptionURI)
372 .withParameters(parms).withParameterGroups(groups).build());
375 private URI getConfigDescriptionURI(HmDevice device) {
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());
386 * Returns true, if the given datapoint can be ignored for metadata generation.
388 public static boolean isIgnoredDatapoint(HmDatapoint dp) {
389 for (String testValue : IGNORE_DATAPOINT_NAMES) {
390 if (dp.getName().contains(testValue)) {