2 * Copyright (c) 2010-2024 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.tacmi.internal.schema;
15 import java.util.LinkedHashMap;
18 import org.attoparser.ParseException;
19 import org.attoparser.simple.AbstractSimpleMarkupHandler;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.tacmi.internal.schema.ChangerX2Entry.OptionType;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
27 * The {@link ApiPageParser} class parses the 'changerx2' page from the CMI and
28 * maps it to the results
30 * @author Christian Niessner - Initial contribution
33 public class ChangerX2Parser extends AbstractSimpleMarkupHandler {
35 private final Logger logger = LoggerFactory.getLogger(ChangerX2Parser.class);
37 static enum ParserState {
46 private final String channelName;
47 private @Nullable String curOptionId;
48 private ParserState parserState = ParserState.INIT;
49 private @Nullable String address;
50 private @Nullable String addressFieldName;
51 private @Nullable String optionFieldName;
52 private @Nullable OptionType optionType;
53 private @Nullable StringBuilder curOptionValue;
54 private Map<String, @Nullable String> options;
56 public ChangerX2Parser(String channelName) {
58 this.options = new LinkedHashMap<>();
59 this.channelName = channelName;
63 public void handleDocumentStart(final long startTimeNanos, final int line, final int col) throws ParseException {
64 this.parserState = ParserState.INIT;
69 public void handleDocumentEnd(final long endTimeNanos, final long totalTimeNanos, final int line, final int col)
70 throws ParseException {
71 if (this.parserState != ParserState.INIT) {
72 logger.debug("Parserstate == Init expected, but is {}", this.parserState);
78 public void handleStandaloneElement(final String elementName, final Map<String, String> attributes,
79 final boolean minimized, final int line, final int col) throws ParseException {
80 logger.debug("Error parsing options for {}: Unexpected StandaloneElement in {}{}: {} [{}]", channelName, line,
81 col, elementName, attributes);
86 public void handleOpenElement(final String elementName, final Map<String, String> attributes, final int line,
87 final int col) throws ParseException {
88 String id = attributes == null ? null : attributes.get("id");
90 if (this.parserState == ParserState.INIT && "input".equals(elementName) && "changeadr".equals(id)) {
91 this.parserState = ParserState.INPUT;
92 if (attributes == null) {
94 this.addressFieldName = null;
96 this.addressFieldName = attributes.get("name");
97 this.address = attributes.get("value");
99 } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
100 && "select".equals(elementName)) {
101 this.parserState = ParserState.SELECT;
102 this.optionFieldName = attributes == null ? null : attributes.get("name");
103 } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
104 && "br".equals(elementName)) {
106 } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
107 && "input".equals(elementName) && "changeto".equals(id)) {
108 this.parserState = ParserState.INPUT_DATA;
109 if (attributes != null) {
110 this.optionFieldName = attributes.get("name");
111 String type = attributes.get("type");
112 if ("number".equals(type)) {
113 this.optionType = OptionType.NUMBER;
114 // we transfer the limits from the input element...
115 this.options.put(ChangerX2Entry.NUMBER_MIN, attributes.get(ChangerX2Entry.NUMBER_MIN));
116 this.options.put(ChangerX2Entry.NUMBER_MAX, attributes.get(ChangerX2Entry.NUMBER_MAX));
117 this.options.put(ChangerX2Entry.NUMBER_STEP, attributes.get(ChangerX2Entry.NUMBER_STEP));
119 logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line,
123 } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
124 && "input".equals(elementName) && "changetotimeh".equals(id)) {
125 this.parserState = ParserState.INPUT_DATA;
126 if (attributes != null) {
127 this.optionFieldName = attributes.get("name");
128 String type = attributes.get("type");
129 if ("number".equals(attributes.get("type"))) {
130 this.optionType = OptionType.TIME;
131 // validate hour limits
132 if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN))
133 || !"24".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) {
135 "Error parsing options for {}: Unexpected MIN/MAX values for hour input field in {}:{}: {}",
136 channelName, line, col, attributes);
140 logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line,
144 } else if ((this.parserState == ParserState.INPUT_DATA || this.parserState == ParserState.INPUT)
145 && "input".equals(elementName) && "changetotimem".equals(id)) {
146 this.parserState = ParserState.INPUT_DATA;
147 if (attributes != null) {
148 if ("number".equals(attributes.get("type"))) {
149 this.optionType = OptionType.TIME;
150 if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN))
151 || !"59".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) {
153 "Error parsing options for {}: Unexpected MIN/MAX values for minute input field in {}:{}: {}",
154 channelName, line, col, attributes);
158 logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line,
162 } else if (this.parserState == ParserState.SELECT && "option".equals(elementName)) {
163 this.parserState = ParserState.SELECT_OPTION;
164 this.optionType = OptionType.SELECT;
165 this.curOptionValue = new StringBuilder();
166 this.curOptionId = attributes == null ? null : attributes.get("value");
168 logger.debug("Error parsing options for {}: Unexpected OpenElement in {}:{}: {} [{}]", channelName, line,
169 col, elementName, attributes);
174 public void handleCloseElement(final @Nullable String elementName, final int line, final int col)
175 throws ParseException {
176 if (this.parserState == ParserState.INPUT && "input".equals(elementName)) {
177 this.parserState = ParserState.INIT;
178 } else if (this.parserState == ParserState.INPUT_DATA && "input".equals(elementName)) {
179 this.parserState = ParserState.INPUT;
180 } else if (this.parserState == ParserState.SELECT && "select".equals(elementName)) {
181 this.parserState = ParserState.INIT;
182 } else if (this.parserState == ParserState.SELECT_OPTION && "option".equals(elementName)) {
183 this.parserState = ParserState.SELECT;
184 StringBuilder sb = this.curOptionValue;
185 String value = sb != null && sb.length() > 0 ? sb.toString().trim() : null;
186 this.curOptionValue = null;
187 String id = this.curOptionId;
188 this.curOptionId = null;
190 if (id == null || id.trim().isEmpty()) {
191 logger.debug("Error parsing options for {}: Got option with empty 'value' in {}:{}: [{}]",
192 channelName, line, col, value);
195 // we use the value as key and the id as value, as we have to map from the value to the id...
197 String prev = this.options.put(value, id);
198 if (prev != null && !prev.equals(value)) {
199 logger.debug("Error parsing options for {}: Got duplicate options in {}:{} for {}: {} and {}",
200 channelName, line, col, value, prev, id);
203 } else if (this.parserState == ParserState.INPUT && "span".equals(elementName)) {
204 // span's are ignored...
206 logger.debug("Error parsing options for {}: Unexpected CloseElement in {}:{}: {}", channelName, line, col,
212 public void handleAutoCloseElement(final @Nullable String elementName, final int line, final int col)
213 throws ParseException {
214 logger.debug("Unexpected AutoCloseElement in {}:{}: {}", line, col,
215 elementName == null ? "<null>" : elementName);
219 public void handleUnmatchedCloseElement(final @Nullable String elementName, final int line, final int col)
220 throws ParseException {
221 logger.debug("Unexpected UnmatchedCloseElement in {}:{}: {}", line, col,
222 elementName == null ? "<null>" : elementName);
226 public void handleDocType(final @Nullable String elementName, final @Nullable String publicId,
227 final @Nullable String systemId, final @Nullable String internalSubset, final int line, final int col)
228 throws ParseException {
229 logger.debug("Unexpected DocType in {}:{}: {}/{}/{}/{}", line, col, elementName, publicId, systemId,
234 public void handleComment(final char @Nullable [] buffer, final int offset, final int len, final int line,
235 final int col) throws ParseException {
236 logger.debug("Unexpected comment in {}:{}: {}", line, col,
237 buffer == null ? "<null>" : new String(buffer, offset, len));
241 public void handleCDATASection(final char @Nullable [] buffer, final int offset, final int len, final int line,
242 final int col) throws ParseException {
243 logger.debug("Unexpected CDATA in {}:{}: {}", line, col,
244 buffer == null ? "<null>" : new String(buffer, offset, len));
248 public void handleText(final char @Nullable [] buffer, final int offset, final int len, final int line,
249 final int col) throws ParseException {
250 if (buffer == null) {
254 if (this.parserState == ParserState.SELECT_OPTION) {
255 // logger.debug("Text {}:{}: {}", line, col, new String(buffer, offset, len));
256 StringBuilder sb = this.curOptionValue;
258 sb.append(buffer, offset, len);
260 } else if (this.parserState == ParserState.INIT && len == 1 && buffer[offset] == '\n') {
261 // single newline - ignore/drop it...
262 } else if (this.parserState == ParserState.INPUT) {
263 // this is a label next to the value input field - we currently have no use for it so
266 logger.debug("Error parsing options for {}: Unexpected Text {}:{}: (ctx: {} len: {}) '{}' ",
267 this.channelName, line, col, this.parserState, len, new String(buffer, offset, len));
272 public void handleXmlDeclaration(final @Nullable String version, final @Nullable String encoding,
273 final @Nullable String standalone, final int line, final int col) throws ParseException {
274 logger.debug("Unexpected XML Declaration {}:{}: {} {} {}", line, col, version, encoding, standalone);
278 public void handleProcessingInstruction(final @Nullable String target, final @Nullable String content,
279 final int line, final int col) throws ParseException {
280 logger.debug("Unexpected ProcessingInstruction {}:{}: {} {}", line, col, target, content);
284 protected ChangerX2Entry getParsedEntry() {
285 String addressFieldName = this.addressFieldName;
286 String address = this.address;
287 String optionFieldName = this.optionFieldName;
288 OptionType optionType = this.optionType;
289 if (address == null || addressFieldName == null || optionType == null || optionFieldName == null) {
292 return new ChangerX2Entry(addressFieldName, address, optionFieldName, optionType, this.options);