]> git.basschouten.com Git - openhab-addons.git/blob
9fa37d8541319905f98b7aa2d587afc5baba3a12
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.tacmi.internal.schema;
14
15 import java.math.BigDecimal;
16 import java.net.URI;
17 import java.net.URISyntaxException;
18 import java.util.ArrayList;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Objects;
24 import java.util.Set;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeoutException;
27
28 import org.attoparser.ParseException;
29 import org.attoparser.simple.AbstractSimpleMarkupHandler;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.util.StringUtil;
33 import org.openhab.binding.tacmi.internal.TACmiBindingConstants;
34 import org.openhab.binding.tacmi.internal.TACmiChannelTypeProvider;
35 import org.openhab.binding.tacmi.internal.schema.ApiPageEntry.Type;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.library.unit.SIUnits;
41 import org.openhab.core.library.unit.Units;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.binding.builder.ChannelBuilder;
45 import org.openhab.core.thing.type.ChannelType;
46 import org.openhab.core.thing.type.ChannelTypeBuilder;
47 import org.openhab.core.thing.type.ChannelTypeUID;
48 import org.openhab.core.types.State;
49 import org.openhab.core.types.StateDescriptionFragmentBuilder;
50 import org.openhab.core.types.StateOption;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 /**
55  * The {@link ApiPageParser} class parses the 'API' schema page from the CMI and
56  * maps it to our channels
57  *
58  * @author Christian Niessner - Initial contribution
59  */
60 @NonNullByDefault
61 public class ApiPageParser extends AbstractSimpleMarkupHandler {
62
63     private final Logger logger = LoggerFactory.getLogger(ApiPageParser.class);
64
65     static enum ParserState {
66         INIT,
67         DATA_ENTRY
68     }
69
70     static enum FieldType {
71         UNKNOWN,
72         READ_ONLY,
73         FORM_VALUE,
74         BUTTON,
75         IGNORE
76     }
77
78     static enum ButtonValue {
79         UNKNOWN,
80         ON,
81         OFF
82     }
83
84     private ParserState parserState = ParserState.INIT;
85     private TACmiSchemaHandler taCmiSchemaHandler;
86     private TACmiChannelTypeProvider channelTypeProvider;
87     private boolean configChanged = false;
88     private FieldType fieldType = FieldType.UNKNOWN;
89     private @Nullable String id;
90     private @Nullable String address;
91     private @Nullable StringBuilder value;
92     private ButtonValue buttonValue = ButtonValue.UNKNOWN;
93     private Map<String, ApiPageEntry> entries;
94     private Set<String> seenNames = new HashSet<>();
95     private List<Channel> channels = new ArrayList<>();
96     // Time stamp when status request was started.
97     private final long statusRequestStartTS;
98     private static @Nullable URI configDescriptionUriAPISchemaDefaults;
99
100     public ApiPageParser(TACmiSchemaHandler taCmiSchemaHandler, Map<String, ApiPageEntry> entries,
101             TACmiChannelTypeProvider channelTypeProvider) {
102         super();
103         this.taCmiSchemaHandler = taCmiSchemaHandler;
104         this.entries = entries;
105         this.channelTypeProvider = channelTypeProvider;
106         this.statusRequestStartTS = System.currentTimeMillis();
107         if (configDescriptionUriAPISchemaDefaults == null) {
108             try {
109                 configDescriptionUriAPISchemaDefaults = new URI(
110                         TACmiBindingConstants.CONFIG_DESCRIPTION_API_SCHEMA_DEFAULTS);
111             } catch (URISyntaxException ex) {
112                 logger.warn("Can't create ConfigDescription URI '{}', ConfigDescription for channels not avilable!",
113                         TACmiBindingConstants.CONFIG_DESCRIPTION_API_SCHEMA_DEFAULTS);
114             }
115         }
116     }
117
118     @Override
119     public void handleDocumentStart(final long startTimeNanos, final int line, final int col) throws ParseException {
120         this.parserState = ParserState.INIT;
121         this.seenNames.clear();
122         this.channels.clear();
123     }
124
125     @Override
126     public void handleDocumentEnd(final long endTimeNanos, final long totalTimeNanos, final int line, final int col)
127             throws ParseException {
128         if (this.parserState != ParserState.INIT) {
129             logger.debug("Parserstate == Init expected, but is {}", this.parserState);
130         }
131     }
132
133     @Override
134     @NonNullByDefault({})
135     public void handleStandaloneElement(final @Nullable String elementName,
136             final @Nullable Map<String, String> attributes, final boolean minimized, final int line, final int col)
137             throws ParseException {
138         logger.debug("Unexpected StandaloneElement in {}:{}: {} [{}]", line, col, elementName, attributes);
139     }
140
141     @Override
142     @NonNullByDefault({})
143     public void handleOpenElement(final @Nullable String elementName, final @Nullable Map<String, String> attributes,
144             final int line, final int col) throws ParseException {
145         if (this.parserState == ParserState.INIT && "div".equals(elementName)) {
146             this.parserState = ParserState.DATA_ENTRY;
147             String classFlags;
148             if (attributes == null) {
149                 classFlags = null;
150                 this.id = null;
151                 this.address = null;
152             } else {
153                 this.id = attributes.get("id");
154                 this.address = attributes.get("adresse");
155                 classFlags = attributes.get("class");
156             }
157             this.fieldType = FieldType.READ_ONLY;
158             this.value = new StringBuilder();
159             this.buttonValue = ButtonValue.UNKNOWN;
160             if (classFlags != null && StringUtil.isNotBlank(classFlags)) {
161                 String[] classFlagList = classFlags.split("[ \n\r]");
162                 for (String classFlag : classFlagList) {
163                     if ("changex2".equals(classFlag)) {
164                         this.fieldType = FieldType.FORM_VALUE;
165                     } else if ("buttonx2".equals(classFlag) || "taster".equals(classFlag)) {
166                         this.fieldType = FieldType.BUTTON;
167                     } else if ("visible0".equals(classFlag)) {
168                         this.buttonValue = ButtonValue.OFF;
169                     } else if ("visible1".equals(classFlag)) {
170                         this.buttonValue = ButtonValue.ON;
171                     } else if ("durchsichtig".equals(classFlag)) { // link
172                         this.fieldType = FieldType.IGNORE;
173                     } else if ("bord".equals(classFlag)) { // special button style - not of our interest...
174                     } else {
175                         logger.debug("Unhanndled class in {}:{}:{}: '{}' ", id, line, col, classFlag);
176                     }
177                 }
178             }
179         } else if (this.parserState == ParserState.DATA_ENTRY && this.fieldType == FieldType.BUTTON
180                 && "span".equals(elementName)) {
181             // ignored...
182         } else {
183             logger.debug("Unexpected OpenElement in {}:{}: {} [{}]", line, col, elementName, attributes);
184         }
185     }
186
187     @Override
188     public void handleCloseElement(final @Nullable String elementName, final int line, final int col)
189             throws ParseException {
190         if (this.parserState == ParserState.DATA_ENTRY && "div".equals(elementName)) {
191             this.parserState = ParserState.INIT;
192             StringBuilder sb = this.value;
193             this.value = null;
194             if (sb != null) {
195                 while (sb.length() > 0 && sb.charAt(0) == ' ') {
196                     sb = sb.delete(0, 0);
197                 }
198                 if (this.fieldType == FieldType.READ_ONLY || this.fieldType == FieldType.FORM_VALUE) {
199                     int lids = sb.lastIndexOf(":");
200                     int fsp = sb.indexOf(" ");
201                     if (fsp < 0 || lids < 0 || fsp > lids) {
202                         logger.debug("Invalid format for setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType,
203                                 sb);
204                     } else {
205                         String shortName = sb.substring(0, fsp).trim();
206                         String description = sb.substring(fsp + 1, lids).trim();
207                         String value = sb.substring(lids + 1).trim();
208                         getApiPageEntry(id, line, col, shortName, description, value);
209                     }
210                 } else if (this.fieldType == FieldType.BUTTON) {
211                     String sbt = sb.toString().trim().replaceAll("[\r\n ]+", " ");
212                     int fsp = sbt.indexOf(" ");
213
214                     if (fsp < 0) {
215                         logger.debug("Invalid format for setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType,
216                                 sbt);
217                     } else {
218                         String shortName = sbt.substring(0, fsp).trim();
219                         String description = sbt.substring(fsp + 1).trim();
220                         getApiPageEntry(id, line, col, shortName, description, this.buttonValue);
221                     }
222                 } else if (this.fieldType == FieldType.IGNORE) {
223                     // ignore
224                 } else {
225                     logger.debug("Unhandled setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType, sb);
226                 }
227             }
228         } else if (this.parserState == ParserState.DATA_ENTRY && this.fieldType == FieldType.BUTTON
229                 && "span".equals(elementName)) {
230             // ignored...
231         } else {
232             logger.debug("Unexpected CloseElement in {}:{}: {}", line, col, elementName);
233         }
234     }
235
236     @Override
237     public void handleAutoCloseElement(final @Nullable String elementName, final int line, final int col)
238             throws ParseException {
239         logger.debug("Unexpected AutoCloseElement in {}:{}: {}", line, col, elementName);
240     }
241
242     @Override
243     public void handleUnmatchedCloseElement(final @Nullable String elementName, final int line, final int col)
244             throws ParseException {
245         logger.debug("Unexpected UnmatchedCloseElement in {}:{}: {}", line, col, elementName);
246     }
247
248     @Override
249     public void handleDocType(final @Nullable String elementName, final @Nullable String publicId,
250             final @Nullable String systemId, final @Nullable String internalSubset, final int line, final int col)
251             throws ParseException {
252         logger.debug("Unexpected DocType in {}:{}: {}/{}/{}/{}", line, col, elementName, publicId, systemId,
253                 internalSubset);
254     }
255
256     @Override
257     public void handleComment(final char @Nullable [] buffer, final int offset, final int len, final int line,
258             final int col) throws ParseException {
259         logger.debug("Unexpected comment in {}:{}: {}", line, col,
260                 buffer == null ? "<null>" : new String(buffer, offset, len));
261     }
262
263     @Override
264     public void handleCDATASection(final char @Nullable [] buffer, final int offset, final int len, final int line,
265             final int col) throws ParseException {
266         logger.debug("Unexpected CDATA in {}:{}: {}", line, col,
267                 buffer == null ? "<null>" : new String(buffer, offset, len));
268     }
269
270     @Override
271     public void handleText(final char @Nullable [] buffer, final int offset, final int len, final int line,
272             final int col) throws ParseException {
273         if (buffer == null) {
274             return;
275         }
276
277         if (this.parserState == ParserState.DATA_ENTRY) {
278             // we append it to our current value
279             StringBuilder sb = this.value;
280             if (sb != null) {
281                 sb.append(buffer, offset, len);
282             }
283         } else if (this.parserState == ParserState.INIT && ((len == 1 && buffer[offset] == '\n')
284                 || (len == 2 && buffer[offset] == '\r' && buffer[offset + 1] == '\n'))) {
285             // single newline - ignore/drop it...
286         } else {
287             String msg = new String(buffer, offset, len).replace("\n", "\\n").replace("\r", "\\r");
288             logger.debug("Unexpected Text {}:{}: ParserState: {} ({}) `{}`", line, col, parserState, len, msg);
289         }
290     }
291
292     @Override
293     public void handleXmlDeclaration(final @Nullable String version, final @Nullable String encoding,
294             final @Nullable String standalone, final int line, final int col) throws ParseException {
295         logger.debug("Unexpected XML Declaration {}:{}: {} {} {}", line, col, version, encoding, standalone);
296     }
297
298     @Override
299     public void handleProcessingInstruction(final @Nullable String target, final @Nullable String content,
300             final int line, final int col) throws ParseException {
301         logger.debug("Unexpected ProcessingInstruction {}:{}: {} {}", line, col, target, content);
302     }
303
304     private void getApiPageEntry(@Nullable String id2, int line, int col, String shortName, String description,
305             Object value) {
306         if (logger.isTraceEnabled()) {
307             logger.trace("Found parameter {}:{}:{} [{}] : {} \"{}\" = {}", id, line, col, this.fieldType, shortName,
308                     description, value);
309         }
310         if (!this.seenNames.add(shortName)) {
311             logger.warn("Found duplicate parameter '{}' in {}:{}:{} [{}] : {} \"{}\" = {}", shortName, id, line, col,
312                     this.fieldType, shortName, description, value);
313             return;
314         }
315
316         if (value instanceof String && ((String) value).contains("can_busy")) {
317             return; // special state to indicate value currently cannot be retrieved..
318         }
319         ApiPageEntry.Type type;
320         State state;
321         String channelType;
322         ChannelTypeUID ctuid;
323         switch (this.fieldType) {
324             case BUTTON:
325                 type = Type.SWITCH_BUTTON;
326                 state = OnOffType.from(this.buttonValue == ButtonValue.ON);
327                 ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RW_UID;
328                 channelType = "Switch";
329                 break;
330             case READ_ONLY:
331             case FORM_VALUE:
332                 String vs = (String) value;
333                 boolean isOn = "ON".equals(vs) || "EIN".equals(vs); // C.M.I. mixes up languages...
334                 if (isOn || "OFF".equals(vs) || "AUS".equals(vs)) {
335                     channelType = "Switch";
336                     state = OnOffType.from(isOn);
337                     if (this.fieldType == FieldType.READ_ONLY || this.address == null) {
338                         ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RO_UID;
339                         type = Type.READ_ONLY_SWITCH;
340                     } else {
341                         ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RW_UID;
342                         type = Type.SWITCH_FORM;
343                     }
344                 } else {
345                     try {
346                         // check if we have a numeric value (either with or without unit)
347                         String[] valParts = vs.split(" ");
348                         // It seems for some wired cases the C.M.I. uses different decimal separators for
349                         // different device types. It seems all 'new' X2-Devices use a dot as separator,
350                         // for the older pre-X2 devices (i.e. the UVR 1611) we get a comma. So we
351                         // we replace all ',' with '.' to check if it's a valid number...
352                         String val = valParts[0].replace(',', '.');
353                         BigDecimal bd = new BigDecimal(val);
354                         if (valParts.length == 2) {
355                             if ("°C".equals(valParts[1])) {
356                                 channelType = "Number:Temperature";
357                                 state = new QuantityType<>(bd, SIUnits.CELSIUS);
358                             } else if ("%".equals(valParts[1])) {
359                                 // channelType = "Number:Percent"; Number:Percent is currently not handled...
360                                 channelType = "Number:Dimensionless";
361                                 state = new QuantityType<>(bd, Units.PERCENT);
362                             } else if ("Imp".equals(valParts[1])) {
363                                 // impulses - no idea how to map this to something useful here?
364                                 channelType = "Number";
365                                 state = new DecimalType(bd);
366                             } else if ("V".equals(valParts[1])) {
367                                 channelType = "Number:Voltage";
368                                 state = new QuantityType<>(bd, Units.VOLT);
369                             } else if ("A".equals(valParts[1])) {
370                                 channelType = "Number:Current";
371                                 state = new QuantityType<>(bd, Units.AMPERE);
372                             } else if ("Hz".equals(valParts[1])) {
373                                 channelType = "Number:Frequency";
374                                 state = new QuantityType<>(bd, Units.HERTZ);
375                             } else if ("kW".equals(valParts[1])) {
376                                 channelType = "Number:Power";
377                                 bd = bd.multiply(new BigDecimal(1000));
378                                 state = new QuantityType<>(bd, Units.WATT);
379                             } else if ("kWh".equals(valParts[1])) {
380                                 channelType = "Number:Power";
381                                 bd = bd.multiply(new BigDecimal(1000));
382                                 state = new QuantityType<>(bd, Units.KILOWATT_HOUR);
383                             } else if ("l/h".equals(valParts[1])) {
384                                 channelType = "Number:Volume";
385                                 bd = bd.divide(new BigDecimal(60));
386                                 state = new QuantityType<>(bd, Units.LITRE_PER_MINUTE);
387                             } else {
388                                 channelType = "Number";
389                                 state = new DecimalType(bd);
390                                 logger.debug("Unhandled UoM for channel {} of type {} for '{}': {}", shortName,
391                                         channelType, description, valParts[1]);
392                             }
393                         } else {
394                             channelType = "Number";
395                             state = new DecimalType(bd);
396                         }
397                         if (this.fieldType == FieldType.READ_ONLY || this.address == null) {
398                             ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID;
399                             type = Type.READ_ONLY_NUMERIC;
400                         } else {
401                             ctuid = null;
402                             type = Type.NUMERIC_FORM;
403                         }
404                     } catch (NumberFormatException nfe) {
405                         // not a number...
406                         channelType = "String";
407                         if (this.fieldType == FieldType.READ_ONLY || this.address == null) {
408                             ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_STATE_RO_UID;
409                             type = Type.READ_ONLY_STATE;
410                         } else {
411                             ctuid = null;
412                             type = Type.STATE_FORM;
413                         }
414                         state = new StringType(vs);
415                     }
416                 }
417                 break;
418             case UNKNOWN:
419             case IGNORE:
420                 return;
421             default:
422                 // should't happen but we have to add default for the compiler...
423                 return;
424         }
425         ApiPageEntry e = this.entries.get(shortName);
426         boolean isNewEntry;
427         if (e == null || e.type != type || !channelType.equals(e.channel.getAcceptedItemType())) {
428             @Nullable
429             Channel channel = this.taCmiSchemaHandler.getThing().getChannel(shortName);
430             @Nullable
431             ChangerX2Entry cx2e = null;
432             if (this.fieldType == FieldType.FORM_VALUE) {
433                 try {
434                     URI uri = this.taCmiSchemaHandler.buildUri("INCLUDE/changerx2.cgi?sadrx2=" + address);
435                     final ChangerX2Parser pp = this.taCmiSchemaHandler.parsePage(uri, new ChangerX2Parser(shortName));
436                     cx2e = pp.getParsedEntry();
437                 } catch (final ParseException | RuntimeException ex) {
438                     logger.warn("Error parsing API Scheme: {} ", ex.getMessage(), ex);
439                 } catch (final TimeoutException | InterruptedException | ExecutionException ex) {
440                     logger.warn("Error loading API Scheme: {} ", ex.getMessage());
441                 }
442             }
443             if (channel == null || !Objects.equals(ctuid, channel.getChannelTypeUID())) {
444                 logger.debug("Creating / updating channel {} of type {} for '{}'", shortName, channelType, description);
445                 this.configChanged = true;
446                 ChannelUID channelUID = new ChannelUID(this.taCmiSchemaHandler.getThing().getUID(), shortName);
447                 ChannelBuilder channelBuilder = ChannelBuilder.create(channelUID, channelType);
448                 channelBuilder.withLabel(description);
449                 if (ctuid != null) {
450                     channelBuilder.withType(ctuid);
451                 } else if (cx2e != null) {
452                     ChannelType ct = buildAndRegisterChannelType(shortName, type, cx2e);
453
454                     channelBuilder.withType(ct.getUID());
455                 } else {
456                     logger.warn("Error configurating channel for {}: channeltype cannot be determined!", shortName);
457                 }
458                 channel = channelBuilder.build(); // add configuration property...
459             } else if (ctuid == null && cx2e != null) {
460                 // custom channel type - check if it already exists and recreate when needed...
461                 ChannelTypeUID curCtuid = channel.getChannelTypeUID();
462                 if (curCtuid != null) {
463                     ChannelType ct = channelTypeProvider.getChannelType(curCtuid, null);
464                     if (ct == null) {
465                         buildAndRegisterChannelType(shortName, type, cx2e);
466                     }
467                 }
468             }
469             this.configChanged = true;
470             e = new ApiPageEntry(type, channel, address, cx2e, state);
471             this.entries.put(shortName, e);
472             isNewEntry = true;
473         } else {
474             isNewEntry = false;
475         }
476         this.channels.add(e.channel);
477         // only update the state when there was no state change sent to C.M.I. after we started
478         // polling the state. It might deliver the previous / old state.
479         if (e.getLastCommandTS() < this.statusRequestStartTS) {
480             Number updatePolicyI = (Number) e.channel.getConfiguration().get("updatePolicy");
481             int updatePolicy = updatePolicyI == null ? 0 : updatePolicyI.intValue();
482             switch (updatePolicy) {
483                 case 0: // 'default'
484                 default:
485                     // we do 'On-Fetch' update when channel is changeable, otherwise 'On-Change'
486                     switch (e.type) {
487                         case NUMERIC_FORM:
488                         case STATE_FORM:
489                         case SWITCH_BUTTON:
490                         case SWITCH_FORM:
491                             if (isNewEntry || !state.equals(e.getLastState())) {
492                                 e.setLastState(state);
493                                 this.taCmiSchemaHandler.updateState(e.channel.getUID(), state);
494                             }
495                             break;
496                         case READ_ONLY_NUMERIC:
497                         case READ_ONLY_STATE:
498                         case READ_ONLY_SWITCH:
499                             e.setLastState(state);
500                             this.taCmiSchemaHandler.updateState(e.channel.getUID(), state);
501                             break;
502                     }
503                     break;
504                 case 1: // On-Fetch
505                     e.setLastState(state);
506                     this.taCmiSchemaHandler.updateState(e.channel.getUID(), state);
507                     break;
508                 case 2: // On-Change
509                     if (isNewEntry || !state.equals(e.getLastState())) {
510                         e.setLastState(state);
511                         this.taCmiSchemaHandler.updateState(e.channel.getUID(), state);
512                     }
513                     break;
514             }
515         }
516     }
517
518     private ChannelType buildAndRegisterChannelType(String shortName, Type type, ChangerX2Entry cx2e) {
519         StateDescriptionFragmentBuilder sdb = StateDescriptionFragmentBuilder.create().withReadOnly(type.readOnly);
520         String itemType;
521         switch (cx2e.optionType) {
522             case NUMBER:
523                 itemType = "Number";
524                 String min = cx2e.options.get(ChangerX2Entry.NUMBER_MIN);
525                 if (min != null && !min.trim().isEmpty()) {
526                     sdb.withMinimum(new BigDecimal(min));
527                 }
528                 String max = cx2e.options.get(ChangerX2Entry.NUMBER_MAX);
529                 if (max != null && !max.trim().isEmpty()) {
530                     sdb.withMaximum(new BigDecimal(max));
531                 }
532                 String step = cx2e.options.get(ChangerX2Entry.NUMBER_STEP);
533                 if (step != null && !step.trim().isEmpty()) {
534                     sdb.withStep(new BigDecimal(step));
535                 }
536                 break;
537             case SELECT:
538                 itemType = "String";
539                 for (Entry<String, @Nullable String> entry : cx2e.options.entrySet()) {
540                     String val = entry.getValue();
541                     if (val != null) {
542                         sdb.withOption(new StateOption(val, entry.getKey()));
543                     }
544                 }
545                 break;
546             default:
547                 throw new IllegalStateException();
548         }
549         ChannelTypeBuilder<?> ctb = ChannelTypeBuilder
550                 .state(new ChannelTypeUID(TACmiBindingConstants.BINDING_ID, shortName), shortName, itemType)
551                 .withDescription("Auto-created for " + shortName).withStateDescriptionFragment(sdb.build());
552
553         // add config description URI
554         URI cdu = configDescriptionUriAPISchemaDefaults;
555         if (cdu != null) {
556             ctb = ctb.withConfigDescriptionURI(cdu);
557         }
558
559         ChannelType ct = ctb.build();
560         channelTypeProvider.addChannelType(ct);
561         return ct;
562     }
563
564     protected boolean isConfigChanged() {
565         return this.configChanged;
566     }
567
568     protected List<Channel> getChannels() {
569         return channels;
570     }
571 }