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