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