diff --git a/bundles/org.openhab.binding.tacmi/README.md b/bundles/org.openhab.binding.tacmi/README.md
index 7689bcbdb36eb..e6aeb1b97bb3a 100644
--- a/bundles/org.openhab.binding.tacmi/README.md
+++ b/bundles/org.openhab.binding.tacmi/README.md
@@ -104,7 +104,7 @@ Sample _.sitemap_ snipplet
sitemap heatingTA label="heatingTA"
{
Text item=TACMI_Analog_In_1
- Setpoint item=TACMI_Analog_Out_1 step=5 minValue=15 maxValue=45
+ Setpoint item=TACMI_Analog_Out_1 step=5 minValue=15 maxValue=45
Switch item=TACMI_Digital_In_1
Switch item=TACMI_Digital_Out_1
}
diff --git a/bundles/org.openhab.binding.tacmi/pom.xml b/bundles/org.openhab.binding.tacmi/pom.xml
index 829c6d9f49da1..267e8c6123254 100644
--- a/bundles/org.openhab.binding.tacmi/pom.xml
+++ b/bundles/org.openhab.binding.tacmi/pom.xml
@@ -7,11 +7,19 @@
org.openhab.addons.bundles
org.openhab.addons.reactor.bundles
- 2.5.6-SNAPSHOT
+ 2.5.8-SNAPSHOT
org.openhab.binding.tacmi
openHAB Add-ons :: Bundles :: TA C.M.I. Binding
+
+
+ org.attoparser
+ attoparser
+ 2.0.5.RELEASE
+ compile
+
+
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java
index c4cfcbe53e7cc..abbd9ad7e251b 100644
--- a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java
@@ -30,6 +30,7 @@ public class TACmiBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CMI = new ThingTypeUID(BINDING_ID, "cmi");
public static final ThingTypeUID THING_TYPE_COE_BRIDGE = new ThingTypeUID(BINDING_ID, "coe-bridge");
+ public static final ThingTypeUID THING_TYPE_CMI_SCHEMA = new ThingTypeUID(BINDING_ID, "cmiSchema");
public static final ChannelTypeUID CHANNEL_TYPE_COE_DIGITAL_IN_UID = new ChannelTypeUID(BINDING_ID,
"coe-digital-in");
@@ -40,6 +41,15 @@ public class TACmiBindingConstants {
public static final ChannelTypeUID CHANNEL_TYPE_COE_ANALOG_OUT_UID = new ChannelTypeUID(BINDING_ID,
"coe-analog-out");
+ public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_SWITCH_RO_UID = new ChannelTypeUID(BINDING_ID,
+ "schema-switch-ro");
+ public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_SWITCH_RW_UID = new ChannelTypeUID(BINDING_ID,
+ "schema-switch-rw");
+ public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID = new ChannelTypeUID(BINDING_ID,
+ "schema-numeric-ro");
+ public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_STATE_RO_UID = new ChannelTypeUID(BINDING_ID,
+ "schema-state-ro");
+
// Channel specific configuration items
public final static String CHANNEL_CONFIG_OUTPUT = "output";
}
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java
new file mode 100644
index 0000000000000..326096545593e
--- /dev/null
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tacmi.internal;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.type.ChannelType;
+import org.eclipse.smarthome.core.thing.type.ChannelTypeProvider;
+import org.eclipse.smarthome.core.thing.type.ChannelTypeUID;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Provides all ChannelTypes for the schema binding...
+ *
+ * @author Christian Niessner (marvkis) - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { TACmiChannelTypeProvider.class, ChannelTypeProvider.class }, immediate = true)
+public class TACmiChannelTypeProvider implements ChannelTypeProvider {
+
+ private final Map channelTypesByUID = new HashMap<>();
+
+ @Override
+ public Collection getChannelTypes(@Nullable Locale locale) {
+ return Collections.unmodifiableCollection(channelTypesByUID.values());
+ }
+
+ @Override
+ public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
+ return channelTypesByUID.get(channelTypeUID);
+ }
+
+ public ChannelType getInternalChannelType(ChannelTypeUID channelTypeUID) {
+ return channelTypesByUID.get(channelTypeUID);
+ }
+
+ public void addChannelType(ChannelType channelType) {
+ channelTypesByUID.put(channelType.getUID(), channelType);
+ }
+}
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java
index 072a37d7d0d84..1ef8fb4be8e0f 100644
--- a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java
@@ -21,13 +21,18 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.eclipse.smarthome.io.net.http.HttpClientFactory;
+import org.openhab.binding.tacmi.internal.schema.TACmiSchemaHandler;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
/**
* The {@link TACmiHandlerFactory} is responsible for creating things and thing
@@ -39,8 +44,18 @@
@Component(configurationPid = "binding.tacmi", service = ThingHandlerFactory.class)
public class TACmiHandlerFactory extends BaseThingHandlerFactory {
- private static final Set SUPPORTED_THING_TYPES_UIDS = Collections
- .unmodifiableSet(Stream.of(THING_TYPE_CMI, THING_TYPE_COE_BRIDGE).collect(Collectors.toSet()));
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
+ Stream.of(THING_TYPE_CMI, THING_TYPE_COE_BRIDGE, THING_TYPE_CMI_SCHEMA).collect(Collectors.toSet()));
+
+ private HttpClient httpClient;
+ private TACmiChannelTypeProvider channelTypeProvider;
+
+ @Activate
+ public TACmiHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+ @Reference TACmiChannelTypeProvider channelTypeProvider) {
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.channelTypeProvider = channelTypeProvider;
+ }
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@@ -55,8 +70,11 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return new TACmiHandler(thing);
} else if (THING_TYPE_COE_BRIDGE.equals(thingTypeUID)) {
return new TACmiCoEBridgeHandler((Bridge) thing);
+ } else if (THING_TYPE_CMI_SCHEMA.equals(thingTypeUID)) {
+ return new TACmiSchemaHandler(thing, httpClient, channelTypeProvider);
}
return null;
}
+
}
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java
new file mode 100644
index 0000000000000..1bf475e9d19c6
--- /dev/null
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tacmi.internal.schema;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.Channel;
+
+/**
+ * The {@link ApiPageEntry} class contains mapping information for an entry of
+ * the API page.
+ *
+ * @author Christian Niessner (marvkis) - Initial contribution
+ */
+@NonNullByDefault
+public class ApiPageEntry {
+
+ static enum Type {
+ ReadOnlySwitch(true), ReadOnlyNumeric(true), NumericForm(false), SwitchButton(false), SwitchForm(false), ReadOnlyState(true), StateForm(false);
+
+ public final boolean readOnly;
+
+ private Type(boolean readOnly) {
+ this.readOnly = readOnly;
+ }
+ }
+
+ /**
+ * type of this entry
+ */
+ public final Type type;
+
+ /**
+ * The channel for this entry
+ */
+ public final Channel channel;
+
+ /**
+ * internal address for this channel
+ */
+ public final @Nullable String address;
+
+ /**
+ * data for handle 'changerx2' form fields
+ */
+ public final @Nullable ChangerX2Entry changerX2Entry;
+
+ protected ApiPageEntry(final Type type, final Channel channel, @Nullable final String address, @Nullable ChangerX2Entry changerX2Entry) {
+ this.type = type;
+ this.channel = channel;
+ this.address = address;
+ this.changerX2Entry = changerX2Entry;
+ }
+}
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java
new file mode 100644
index 0000000000000..b96283572bf56
--- /dev/null
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java
@@ -0,0 +1,481 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tacmi.internal.schema;
+
+import java.math.BigDecimal;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.commons.lang.StringUtils;
+import org.attoparser.ParseException;
+import org.attoparser.simple.AbstractSimpleMarkupHandler;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.library.unit.SIUnits;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+import org.eclipse.smarthome.core.thing.Channel;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.binding.builder.ChannelBuilder;
+import org.eclipse.smarthome.core.thing.type.ChannelType;
+import org.eclipse.smarthome.core.thing.type.ChannelTypeBuilder;
+import org.eclipse.smarthome.core.thing.type.ChannelTypeUID;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.core.types.StateDescriptionFragmentBuilder;
+import org.eclipse.smarthome.core.types.StateOption;
+import org.openhab.binding.tacmi.internal.TACmiBindingConstants;
+import org.openhab.binding.tacmi.internal.TACmiChannelTypeProvider;
+import org.openhab.binding.tacmi.internal.schema.ApiPageEntry.Type;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ApiPageParser} class parses the 'API' schema page from the CMI and
+ * maps it to our channels
+ *
+ * @author Christian Niessner (marvkis) - Initial contribution
+ */
+public class ApiPageParser extends AbstractSimpleMarkupHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(ApiPageParser.class);
+
+ static enum ParserState {
+ Init,
+ DataEntry
+ }
+
+ static enum FieldType {
+ Unknown,
+ ReadOnly,
+ FormValue,
+ Button,
+ Ignore
+ }
+
+ static enum ButtonValue {
+ Unknown,
+ On,
+ Off
+ }
+
+ private @NonNull ParserState parserState = ParserState.Init;
+ private @NonNull TACmiSchemaHandler taCmiSchemaHandler;
+ private @NonNull TACmiChannelTypeProvider channelTypeProvider;
+ private boolean configChanged = false;
+ private @NonNull FieldType fieldType = FieldType.Unknown;
+ private @Nullable String id;
+ private @Nullable String address;
+ private @Nullable StringBuilder value;
+ private @NonNull ButtonValue buttonValue = ButtonValue.Unknown;
+ private @NonNull Map<@NonNull String, @Nullable ApiPageEntry> entries;
+ private @NonNull Set<@NonNull String> seenNames = new HashSet<>();
+ private @NonNull List<@NonNull Channel> channels = new ArrayList<>();
+
+ public ApiPageParser(@NonNull TACmiSchemaHandler taCmiSchemaHandler,
+ @NonNull Map entries,
+ @NonNull TACmiChannelTypeProvider channelTypeProvider) {
+ super();
+ this.taCmiSchemaHandler = taCmiSchemaHandler;
+ this.entries = entries;
+ this.channelTypeProvider = channelTypeProvider;
+ }
+
+ @Override
+ public void handleDocumentStart(final long startTimeNanos, final int line, final int col) throws ParseException {
+ this.parserState = ParserState.Init;
+ this.seenNames.clear();
+ this.channels.clear();
+ }
+
+ @Override
+ public void handleDocumentEnd(final long endTimeNanos, final long totalTimeNanos, final int line, final int col)
+ throws ParseException {
+ if (this.parserState != ParserState.Init) {
+ logger.debug("Parserstate == Init expected, but is {}", this.parserState);
+ }
+ }
+
+ @Override
+ public void handleStandaloneElement(final @Nullable String elementName,
+ final @Nullable Map attributes, final boolean minimized, final int line, final int col)
+ throws ParseException {
+
+ logger.info("Unexpected StandaloneElement in {}:{}: {} [{}]", line, col, elementName, attributes);
+ }
+
+ @Override
+ public void handleOpenElement(final @Nullable String elementName, final @Nullable Map attributes,
+ final int line, final int col) throws ParseException {
+
+ if (this.parserState == ParserState.Init && "div".equals(elementName)) {
+ this.parserState = ParserState.DataEntry;
+ String classFlags;
+ if (attributes == null) {
+ classFlags = null;
+ this.id = null;
+ this.address = null;
+ } else {
+ this.id = attributes.get("id");
+ this.address = attributes.get("adresse");
+ classFlags = attributes.get("class");
+ }
+ this.fieldType = FieldType.ReadOnly;
+ this.value = new StringBuilder();
+ this.buttonValue = ButtonValue.Unknown;
+ if (classFlags != null && StringUtil.isNotBlank(classFlags)) {
+ String[] classFlagList = classFlags.split("[ \n\r]");
+ for (String classFlag : classFlagList) {
+ if ("changex2".equals(classFlag)) {
+ this.fieldType = FieldType.FormValue;
+ } else if ("buttonx2".equals(classFlag) || "taster".equals(classFlag)) {
+ this.fieldType = FieldType.Button;
+ } else if ("visible0".equals(classFlag)) {
+ this.buttonValue = ButtonValue.Off;
+ } else if ("visible1".equals(classFlag)) {
+ this.buttonValue = ButtonValue.On;
+ } else if ("durchsichtig".equals(classFlag)) { // link
+ this.fieldType = FieldType.Ignore;
+ } else if ("bord".equals(classFlag)) { // special button style - not of our interest...
+ } else {
+ logger.debug("Unhanndled class in {}:{}:{}: '{}' ", id, line, col, classFlag);
+ }
+ }
+ }
+ } else if (this.parserState == ParserState.DataEntry && this.fieldType == FieldType.Button
+ && "span".equals(elementName)) {
+ // ignored...
+ } else {
+ logger.info("Unexpected OpenElement in {}:{}: {} [{}]", line, col, elementName, attributes);
+ }
+ }
+
+ @Override
+ public void handleCloseElement(final @Nullable String elementName, final int line, final int col)
+ throws ParseException {
+ if (this.parserState == ParserState.DataEntry && "div".equals(elementName)) {
+ this.parserState = ParserState.Init;
+ StringBuilder sb = this.value;
+ this.value = null;
+ if (sb != null) {
+ while (sb.length() > 0 && sb.charAt(0) == ' ') {
+ sb = sb.delete(0, 0);
+ }
+ if (this.fieldType == FieldType.ReadOnly || this.fieldType == FieldType.FormValue) {
+ int lids = sb.lastIndexOf(":");
+ int fsp = sb.indexOf(" ");
+ if (fsp < 0 || lids < 0 || fsp > lids) {
+ logger.info("Invalid format for setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType, sb);
+ } else {
+ String shortName = sb.substring(0, fsp).trim();
+ String description = sb.substring(fsp + 1, lids).trim();
+ String value = sb.substring(lids + 1).trim();
+ getApiPageEntry(id, line, col, shortName, description, value);
+ }
+ } else if (this.fieldType == FieldType.Button) {
+ String sbt = sb.toString().trim().replaceAll("[\r\n ]+", " ");
+ int fsp = sbt.indexOf(" ");
+
+ if (fsp < 0) {
+ logger.info("Invalid format for setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType,
+ sbt);
+ } else {
+ String shortName = sbt.substring(0, fsp).trim();
+ String description = sbt.substring(fsp + 1).trim();
+ getApiPageEntry(id, line, col, shortName, description, this.buttonValue);
+ }
+ } else if (this.fieldType == FieldType.Ignore) {
+ // ignore
+ } else {
+ logger.info("Unhandled setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType, sb);
+ }
+ }
+ } else if (this.parserState == ParserState.DataEntry && this.fieldType == FieldType.Button
+ && "span".equals(elementName)) {
+ // ignored...
+ } else {
+ logger.info("Unexpected CloseElement in {}:{}: {}", line, col, elementName);
+ }
+ }
+
+ @Override
+ public void handleAutoCloseElement(final @Nullable String elementName, final int line, final int col)
+ throws ParseException {
+ logger.info("Unexpected AutoCloseElement in {}:{}: {}", line, col, elementName);
+ }
+
+ @Override
+ public void handleUnmatchedCloseElement(final @Nullable String elementName, final int line, final int col)
+ throws ParseException {
+ logger.info("Unexpected UnmatchedCloseElement in {}:{}: {}", line, col, elementName);
+ }
+
+ @Override
+ public void handleDocType(final @Nullable String elementName, final @Nullable String publicId,
+ final @Nullable String systemId, final @Nullable String internalSubset, final int line, final int col)
+ throws ParseException {
+ logger.info("Unexpected DocType in {}:{}: {}/{}/{}/{}", line, col, elementName, publicId, systemId,
+ internalSubset);
+ }
+
+ @Override
+ public void handleComment(final char @Nullable [] buffer, final int offset, final int len, final int line,
+ final int col) throws ParseException {
+ logger.info("Unexpected comment in {}:{}: {}", line, col, new String(buffer, offset, len));
+ }
+
+ @Override
+ public void handleCDATASection(final char @Nullable [] buffer, final int offset, final int len, final int line,
+ final int col) throws ParseException {
+ logger.info("Unexpected CDATA in {}:{}: {}", line, col, new String(buffer, offset, len));
+ }
+
+ @Override
+ public void handleText(final char @Nullable [] buffer, final int offset, final int len, final int line,
+ final int col) throws ParseException {
+
+ if (buffer == null) {
+ return;
+ }
+
+ if (this.parserState == ParserState.DataEntry) {
+ // logger.debug("Text {}:{}: {}", line, col, new String(buffer, offset, len));
+ StringBuilder sb = this.value;
+ if (sb != null) {
+ sb.append(buffer, offset, len);
+ }
+ } else if (this.parserState == ParserState.Init && len == 1 && buffer[offset] == '\n') {
+ // single newline - ignore/drop it...
+ } else {
+ logger.info("Unexpected Text {}:{}: ({}) {} ", line, col, len, new String(buffer, offset, len));
+ }
+ }
+
+ @Override
+ public void handleXmlDeclaration(final @Nullable String version, final @Nullable String encoding,
+ final @Nullable String standalone, final int line, final int col) throws ParseException {
+ logger.info("Unexpected XML Declaration {}:{}: {} {} {}", line, col, version, encoding, standalone);
+ }
+
+ @Override
+ public void handleProcessingInstruction(final @Nullable String target, final @Nullable String content,
+ final int line, final int col) throws ParseException {
+ logger.info("Unexpected ProcessingInstruction {}:{}: {} {}", line, col, target, content);
+ }
+
+ private void getApiPageEntry(@Nullable String id2, int line, int col, String shortName, String description,
+ Object value) {
+ // this.taCmiSchemaHandler.thingUpdated(thing);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Found parameter {}:{}:{} [{}] : {} \"{}\" = {}", id, line, col, this.fieldType, shortName,
+ description, value);
+ }
+ if (!this.seenNames.add(shortName)) {
+ logger.warn("Found duplicate parameter '{}' in {}:{}:{} [{}] : {} \"{}\" = {}", shortName, id, line, col,
+ this.fieldType, shortName, description, value);
+ return;
+ }
+
+ if (value instanceof String && ((String) value).contains("can_busy")) {
+ return; // special state to indicate value currently not retrieveable..
+ }
+ ApiPageEntry.Type type;
+ State state;
+ String channelType;
+ ChannelTypeUID ctuid;
+ switch (this.fieldType) {
+ case Button:
+ type = Type.SwitchButton;
+ state = this.buttonValue == ButtonValue.On ? OnOffType.ON : OnOffType.OFF;
+ ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RW_UID;
+ channelType = "Switch";
+ break;
+ case ReadOnly:
+ case FormValue:
+ String vs = (String) value;
+ boolean isOn = "ON".equals(vs) || "EIN".equals(vs); // C.M.I. mixes up languages...
+ if (isOn || "OFF".equals(vs) || "AUS".equals(vs)) {
+ channelType = "Switch";
+ state = isOn ? OnOffType.ON : OnOffType.OFF;
+ if (this.fieldType == FieldType.ReadOnly || this.address == null) {
+ ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RO_UID;
+ type = Type.ReadOnlySwitch;
+ } else {
+ ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RW_UID;
+ type = Type.SwitchForm;
+ }
+ } else {
+ try {
+ // check if we have a numeric value (either with or without unit)
+ String[] valParts = vs.split(" ");
+ BigDecimal bd = new BigDecimal(valParts[0]);
+ if (valParts.length == 2) {
+ if ("°C".equals(valParts[1])) {
+ channelType = "Number:Temperature";
+ state = new QuantityType<>(bd, SIUnits.CELSIUS);
+ } else if ("%".equals(valParts[1])) {
+ channelType = "Number:Percent";
+ state = new QuantityType<>(bd, SmartHomeUnits.PERCENT);
+ } else if ("Imp".equals(valParts[1])) {
+ // impulses - no idea how to map this to something useful here?
+ channelType = "Number";
+ state = new DecimalType(bd);
+ } else if ("V".equals(valParts[1])) {
+ channelType = "Number:Voltage";
+ state = new QuantityType<>(bd, SmartHomeUnits.VOLT);
+ } else if ("A".equals(valParts[1])) {
+ channelType = "Number:Current";
+ state = new QuantityType<>(bd, SmartHomeUnits.AMPERE);
+ } else if ("kW".equals(valParts[1])) {
+ channelType = "Number:Power";
+ bd = bd.multiply(new BigDecimal(1000));
+ state = new QuantityType<>(bd, SmartHomeUnits.WATT);
+ } else if ("kWh".equals(valParts[1])) {
+ channelType = "Number:Power";
+ bd = bd.multiply(new BigDecimal(1000));
+ state = new QuantityType<>(bd, SmartHomeUnits.KILOWATT_HOUR);
+ } else if ("l/h".equals(valParts[1])) {
+ channelType = "Number:Volume";
+ bd = bd.divide(new BigDecimal(60));
+ state = new QuantityType<>(bd, SmartHomeUnits.LITRE_PER_MINUTE);
+ } else {
+ channelType = "Number";
+ state = new DecimalType(bd);
+ logger.info("Unhandled UoM for channel {} of type {} for '{}': {}", shortName,
+ channelType, description, valParts[1]);
+ }
+ } else {
+ channelType = "Number";
+ state = new DecimalType(bd);
+ }
+ if (this.fieldType == FieldType.ReadOnly || this.address == null) {
+ ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID;
+ type = Type.ReadOnlyNumeric;
+ } else {
+ ctuid = null;
+ type = Type.NumericForm;
+ }
+ } catch (NumberFormatException nfe) {
+ // not a number...
+ channelType = "String";
+ if (this.fieldType == FieldType.ReadOnly || this.address == null) {
+ ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_STATE_RO_UID;
+ type = Type.ReadOnlyState;
+ } else {
+ ctuid = null;
+ type = Type.StateForm;
+ }
+ state = new StringType(vs);
+ }
+ }
+ break;
+ case Unknown:
+ case Ignore:
+ return;
+ default:
+ // should't happen but we have to add default for the compiler...
+ return;
+ }
+ ApiPageEntry e = this.entries.get(shortName);
+ if (e == null || e.type != type || !channelType.equals(e.channel.getAcceptedItemType())) {
+ @Nullable
+ Channel channel = this.taCmiSchemaHandler.getThing().getChannel(shortName);
+ @Nullable
+ ChangerX2Entry cx2e = null;
+ if (this.fieldType == FieldType.FormValue) {
+ try {
+ URI uri = this.taCmiSchemaHandler.buildUri("INCLUDE/changerx2.cgi?sadrx2=" + address);
+ final ChangerX2Parser pp = this.taCmiSchemaHandler.parsePage(uri, new ChangerX2Parser());
+ cx2e = pp.getParsedEntry();
+ } catch (final Exception ex) {
+ logger.error("Error loading API Scheme: {} ", ex.getMessage(), ex);
+ }
+ }
+ if (channel == null) {
+ logger.info("Creating / updating channel {} of type {} for '{}'", shortName, channelType, description);
+ this.configChanged = true;
+ ChannelUID channelUID = new ChannelUID(this.taCmiSchemaHandler.getThing().getUID(), shortName);
+ ChannelBuilder channelBuilder = ChannelBuilder.create(channelUID, channelType);
+ channelBuilder.withLabel(description);
+ if (ctuid != null) {
+ channelBuilder.withType(ctuid);
+ } else if (cx2e != null) {
+ StateDescriptionFragmentBuilder sdb = StateDescriptionFragmentBuilder.create()
+ .withReadOnly(type.readOnly);
+ String itemType;
+ switch (cx2e.optionType) {
+ case Number:
+ itemType = "Number";
+ String min = cx2e.options.get(ChangerX2Entry.NUMBER_MIN);
+ if (StringUtils.isNotBlank(min)) {
+ sdb.withMinimum(new BigDecimal(min));
+ }
+ String max = cx2e.options.get(ChangerX2Entry.NUMBER_MAX);
+ if (StringUtils.isNotBlank(max)) {
+ sdb.withMaximum(new BigDecimal(max));
+ }
+ String step = cx2e.options.get(ChangerX2Entry.NUMBER_STEP);
+ if (StringUtils.isNotBlank(step)) {
+ sdb.withStep(new BigDecimal(step));
+ }
+ break;
+ case Select:
+ itemType = "String";
+ for (Entry<@NonNull String, @Nullable String> entry : cx2e.options.entrySet()) {
+ String val = entry.getValue();
+ if (val != null) {
+ sdb.withOption(new StateOption(val, entry.getKey()));
+ }
+ }
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ ChannelType ct = ChannelTypeBuilder
+ .state(new ChannelTypeUID(TACmiBindingConstants.BINDING_ID, shortName), shortName, itemType)
+ .withDescription("Auto-created for " + shortName)
+ .withStateDescription(sdb.build().toStateDescription())
+ // .withCategory("CategoryName") can we do something useful ?
+ .build();
+ channelTypeProvider.addChannelType(ct);
+ channelBuilder.withType(ct.getUID());
+ } else {
+ logger.warn("Error configurating channel for {}: channeltype cannot be determined!", shortName);
+ }
+ channel = channelBuilder.build(); // add configuration property...
+ }
+ this.configChanged = true;
+ e = new ApiPageEntry(type, channel, address, cx2e);
+ this.entries.put(shortName, e);
+ }
+ this.channels.add(e.channel);
+ this.taCmiSchemaHandler.updateState(e.channel.getUID(), state);
+ }
+
+ protected boolean isConfigChanged() {
+ return this.configChanged;
+ }
+
+ protected @NonNull List<@NonNull Channel> getChannels() {
+ return channels;
+ }
+
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java
new file mode 100644
index 0000000000000..0aebb78c2f21c
--- /dev/null
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tacmi.internal.schema;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link ChangerX2Entry} class contains mapping information for a changerX2 entry of
+ * the API page element
+ *
+ * @author Christian Niessner (marvkis) - Initial contribution
+ */
+@NonNullByDefault
+public class ChangerX2Entry {
+
+ public static final String NUMBER_MIN = "min";
+ public static final String NUMBER_MAX = "max";
+ public static final String NUMBER_STEP = "step";
+
+ static enum OptionType {
+ Number,
+ Select,
+ }
+
+ /**
+ * field name of the address
+ */
+ public final String addressFieldName;
+
+ /**
+ * The address these options are for
+ */
+ public final String address;
+
+ /**
+ * option type
+ */
+ public final OptionType optionType;
+
+ /**
+ * field name of the option value
+ */
+ public final String optionFieldName;
+
+ /**
+ * the valid options
+ */
+ public final Map options;
+
+ public ChangerX2Entry(String addressFieldName, String address, String optionFieldName, OptionType optionType,
+ Map options) {
+ this.addressFieldName = addressFieldName;
+ this.address = address;
+ this.optionFieldName = optionFieldName;
+ this.optionType = optionType;
+ this.options = options;
+ }
+}
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java
new file mode 100644
index 0000000000000..b49ce71496350
--- /dev/null
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java
@@ -0,0 +1,236 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tacmi.internal.schema;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.attoparser.ParseException;
+import org.attoparser.simple.AbstractSimpleMarkupHandler;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tacmi.internal.schema.ChangerX2Entry.OptionType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ApiPageParser} class parses the 'changerx2' page from the CMI and
+ * maps it to the results
+ *
+ * @author Christian Niessner (marvkis) - Initial contribution
+ */
+public class ChangerX2Parser extends AbstractSimpleMarkupHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(ApiPageParser.class);
+
+ static enum ParserState {
+ Init,
+ Input,
+ InputData,
+ Select,
+ SelectOption,
+ Unknown
+ }
+
+ private @Nullable String curOptionId;
+ private @NonNull ParserState parserState = ParserState.Init;
+ private @Nullable String address;
+ private @Nullable String addressFieldName;
+ private @Nullable String optionFieldName;
+ private @Nullable OptionType optionType;
+ private @Nullable StringBuilder curOptionValue;
+ private @NonNull Map<@NonNull String, @Nullable String> options;
+
+ public ChangerX2Parser() {
+ super();
+ this.options = new LinkedHashMap<>();
+ }
+
+ @Override
+ public void handleDocumentStart(final long startTimeNanos, final int line, final int col) throws ParseException {
+ this.parserState = ParserState.Init;
+ this.options.clear();
+ }
+
+ @Override
+ public void handleDocumentEnd(final long endTimeNanos, final long totalTimeNanos, final int line, final int col)
+ throws ParseException {
+ if (this.parserState != ParserState.Init) {
+ logger.debug("Parserstate == Init expected, but is {}", this.parserState);
+ }
+ }
+
+ @Override
+ public void handleStandaloneElement(final String elementName, final Map attributes,
+ final boolean minimized, final int line, final int col) throws ParseException {
+
+ logger.info("Unexpected StandaloneElement in {}{}: {} [{}]", line, col, elementName, attributes);
+ }
+
+ @Override
+ public void handleOpenElement(final String elementName, final Map attributes, final int line,
+ final int col) throws ParseException {
+
+ String id = attributes == null ? null : attributes.get("id");
+
+ if (this.parserState == ParserState.Init && "input".equals(elementName) && "changeadr".equals(id)) {
+ this.parserState = ParserState.Input;
+ if (attributes == null) {
+ this.address = null;
+ this.addressFieldName = null;
+ } else {
+ this.addressFieldName = attributes.get("name");
+ this.address = attributes.get("value");
+ }
+ } else if ((this.parserState == ParserState.Init || this.parserState == ParserState.Input)
+ && "select".equals(elementName)) {
+ this.parserState = ParserState.Select;
+ this.optionFieldName = attributes == null ? null : attributes.get("name");
+ } else if ((this.parserState == ParserState.Init || this.parserState == ParserState.Input)
+ && "br".equals(elementName)) {
+ // ignored
+ } else if ((this.parserState == ParserState.Init || this.parserState == ParserState.Input)
+ && "input".equals(elementName) && "changeto".equals(id)) {
+ this.parserState = ParserState.InputData;
+ if (attributes != null) {
+ this.optionFieldName = attributes.get("name");
+ String type = attributes.get("type");
+ if ("number".equals(type)) {
+ this.optionType = OptionType.Number;
+ // we transfer the limits from the input elemnt...
+ this.options.put(ChangerX2Entry.NUMBER_MIN, attributes.get(ChangerX2Entry.NUMBER_MIN));
+ this.options.put(ChangerX2Entry.NUMBER_MAX, attributes.get(ChangerX2Entry.NUMBER_MAX));
+ this.options.put(ChangerX2Entry.NUMBER_STEP, attributes.get(ChangerX2Entry.NUMBER_STEP));
+ } else {
+ logger.warn("Unhandled input field in {}:{}: {}", line, col, attributes);
+ }
+ }
+ } else if (this.parserState == ParserState.Select && "option".equals(elementName)) {
+ this.parserState = ParserState.SelectOption;
+ this.optionType = OptionType.Select;
+ this.curOptionValue = new StringBuilder();
+ this.curOptionId = attributes == null ? null : attributes.get("value");
+ } else {
+ logger.info("Unexpected OpenElement in {}:{}: {} [{}]", line, col, elementName, attributes);
+ }
+ }
+
+ @Override
+ public void handleCloseElement(final @Nullable String elementName, final int line, final int col)
+ throws ParseException {
+ if (this.parserState == ParserState.Input && "input".equals(elementName)) {
+ this.parserState = ParserState.Init;
+ } else if (this.parserState == ParserState.Select && "select".equals(elementName)) {
+ this.parserState = ParserState.Init;
+ } else if (this.parserState == ParserState.SelectOption && "option".equals(elementName)) {
+ this.parserState = ParserState.Select;
+ StringBuilder sb = this.curOptionValue;
+ String value = sb != null && sb.length() > 0 ? sb.toString().trim() : null;
+ this.curOptionValue = null;
+ String id = this.curOptionId;
+ this.curOptionId = null;
+ if (value != null) {
+ if (id == null || !StringUtils.isNotBlank(id)) {
+ logger.info("Got option with empty 'value' in {}:{}: [{}]", line, col, value);
+ return;
+ }
+ // we use the value as key and the id as value, as we have to map from the value to the id...
+ @Nullable
+ String prev = this.options.put(value, id);
+ if (prev != null && !prev.equals(value)) {
+ logger.info("Got duplicate options in {}:{} for {}: {} and {}", line, col, value, prev, id);
+ }
+ }
+ } else {
+ logger.info("Unexpected CloseElement in {}:{}: {}", line, col, elementName);
+ }
+ }
+
+ @Override
+ public void handleAutoCloseElement(final @Nullable String elementName, final int line, final int col)
+ throws ParseException {
+ logger.info("Unexpected AutoCloseElement in {}:{}: {}", line, col, elementName);
+ }
+
+ @Override
+ public void handleUnmatchedCloseElement(final @Nullable String elementName, final int line, final int col)
+ throws ParseException {
+ logger.info("Unexpected UnmatchedCloseElement in {}:{}: {}", line, col, elementName);
+ }
+
+ @Override
+ public void handleDocType(final @Nullable String elementName, final @Nullable String publicId,
+ final @Nullable String systemId, final @Nullable String internalSubset, final int line, final int col)
+ throws ParseException {
+ logger.info("Unexpected DocType in {}:{}: {}/{}/{}/{}", line, col, elementName, publicId, systemId,
+ internalSubset);
+ }
+
+ @Override
+ public void handleComment(final char @Nullable [] buffer, final int offset, final int len, final int line,
+ final int col) throws ParseException {
+ logger.info("Unexpected comment in {}:{}: {}", line, col, new String(buffer, offset, len));
+ }
+
+ @Override
+ public void handleCDATASection(final char @Nullable [] buffer, final int offset, final int len, final int line,
+ final int col) throws ParseException {
+ logger.info("Unexpected CDATA in {}:{}: {}", line, col, new String(buffer, offset, len));
+ }
+
+ @Override
+ public void handleText(final char @Nullable [] buffer, final int offset, final int len, final int line,
+ final int col) throws ParseException {
+
+ if (buffer == null) {
+ return;
+ }
+
+ if (this.parserState == ParserState.SelectOption) {
+ // logger.debug("Text {}:{}: {}", line, col, new String(buffer, offset, len));
+ StringBuilder sb = this.curOptionValue;
+ if (sb != null) {
+ sb.append(buffer, offset, len);
+ }
+ } else if (this.parserState == ParserState.Init && len == 1 && buffer[offset] == '\n') {
+ // single newline - ignore/drop it...
+ } else {
+ logger.info("Unexpected Text {}:{}: ({}) {} ", line, col, len, new String(buffer, offset, len));
+ }
+ }
+
+ @Override
+ public void handleXmlDeclaration(final @Nullable String version, final @Nullable String encoding,
+ final @Nullable String standalone, final int line, final int col) throws ParseException {
+ logger.info("Unexpected XML Declaration {}:{}: {} {} {}", line, col, version, encoding, standalone);
+ }
+
+ @Override
+ public void handleProcessingInstruction(final @Nullable String target, final @Nullable String content,
+ final int line, final int col) throws ParseException {
+ logger.info("Unexpected ProcessingInstruction {}:{}: {} {}", line, col, target, content);
+ }
+
+ protected ChangerX2Entry getParsedEntry() {
+ String addressFieldName = this.addressFieldName;
+ String address = this.address;
+ String optionFieldName = this.optionFieldName;
+ OptionType optionType = this.optionType;
+ if (address == null || addressFieldName == null || optionType == null || optionFieldName == null) {
+ return null;
+ }
+ return new ChangerX2Entry(addressFieldName, address, optionFieldName, optionType, this.options);
+ }
+
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java
new file mode 100644
index 0000000000000..7a70d78a066ff
--- /dev/null
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tacmi.internal.schema;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TACmiConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Christian Niessner (marvkis) - Initial contribution
+ */
+@NonNullByDefault
+public class TACmiSchemaConfiguration {
+
+ /**
+ * host address of the C.M.I.
+ */
+ public String host = "";
+
+ /**
+ * Username
+ */
+ public String username = "";
+
+ /**
+ * Password
+ */
+ public String password = "";
+
+ /**
+ * ID of API schema page
+ */
+ public int schemaId;
+
+ /**
+ * API page poll intervall
+ */
+ public int pollInterval;
+
+}
diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java
new file mode 100644
index 0000000000000..f469f9b6d9c80
--- /dev/null
+++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java
@@ -0,0 +1,300 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tacmi.internal.schema;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.attoparser.ParseException;
+import org.attoparser.config.ParseConfiguration;
+import org.attoparser.config.ParseConfiguration.ElementBalancing;
+import org.attoparser.config.ParseConfiguration.UniqueRootElementPresence;
+import org.attoparser.simple.AbstractSimpleMarkupHandler;
+import org.attoparser.simple.ISimpleMarkupParser;
+import org.attoparser.simple.SimpleMarkupParser;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.util.B64Code;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.thing.Channel;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.thing.binding.builder.ThingBuilder;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.RefreshType;
+import org.eclipse.smarthome.core.types.State;
+import org.openhab.binding.tacmi.internal.TACmiChannelTypeProvider;
+import org.openhab.binding.tacmi.internal.TACmiHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TACmiHandler} is responsible for handling commands, which are sent
+ * to one of the channels.
+ *
+ * @author Christian Niessner (marvkis) - Initial contribution
+ */
+@NonNullByDefault
+public class TACmiSchemaHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(TACmiSchemaHandler.class);
+
+ /**
+ * the C.M.I.'s address
+ */
+ // private @Nullable InetAddress cmiAddress;
+
+ private final HttpClient httpClient;
+ private final TACmiChannelTypeProvider channelTypeProvider;
+ private final Map entries = new HashMap<>();
+ private boolean online;
+ private @Nullable String serverBase;
+ private @Nullable URI schemaApiPage;
+ private @Nullable String authHeader;
+ private @Nullable ScheduledFuture> scheduledFuture;
+ private final ParseConfiguration noRestrictions;
+
+ public TACmiSchemaHandler(final Thing thing, final HttpClient httpClient,
+ final TACmiChannelTypeProvider channelTypeProvider) {
+ super(thing);
+ this.httpClient = httpClient;
+ this.channelTypeProvider = channelTypeProvider;
+
+ // the default configuration for the parser
+ this.noRestrictions = ParseConfiguration.xmlConfiguration();
+ this.noRestrictions.setElementBalancing(ElementBalancing.NO_BALANCING);
+ this.noRestrictions.setNoUnmatchedCloseElementsRequired(false);
+ this.noRestrictions.setUniqueAttributesInElementRequired(false);
+ this.noRestrictions.setXmlWellFormedAttributeValuesRequired(false);
+ this.noRestrictions.setUniqueRootElementPresence(UniqueRootElementPresence.NOT_VALIDATED);
+ this.noRestrictions.getPrologParseConfiguration().setValidateProlog(false);
+ }
+
+ @Override
+ public void initialize() {
+ // logger.debug("Start initializing!");
+ final TACmiSchemaConfiguration config = getConfigAs(TACmiSchemaConfiguration.class);
+
+ if (StringUtil.isBlank(config.host)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!");
+ return;
+ }
+ if (StringUtil.isBlank(config.username)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No username configured!");
+ return;
+ }
+ if (StringUtil.isBlank(config.password)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No password configured!");
+ return;
+ }
+ this.online = false;
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING);
+
+ // set cmiAddress from configuration
+ // cmiAddress = (String) configuration.get("cmiAddress");
+ /*
+ * try { cmiAddress = InetAddress.getByName(config.host); } catch (final
+ * UnknownHostException e1) {
+ * logger.error("Failed to get IP of C.M.I. from configuration");
+ * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ * "Failed to get IP of C.M.I. from configuration"); return; }
+ */
+ this.authHeader = "Basic "
+ + B64Code.encode(config.username + ":" + config.password, StandardCharsets.ISO_8859_1);
+
+ final String serverBase = "http://" + config.host + "/";
+ this.serverBase = serverBase;
+ this.schemaApiPage = buildUri("schematic_files/" + config.schemaId + ".cgi");
+
+ refreshData();
+ if (config.pollInterval <= 0) {
+ config.pollInterval = 10;
+ }
+ this.scheduledFuture = scheduler.scheduleAtFixedRate(() -> refreshData(), config.pollInterval,
+ config.pollInterval, TimeUnit.SECONDS);
+ }
+
+ protected URI buildUri(String path) {
+ return URI.create(serverBase + path);
+ }
+
+ private Request prepareRequest(final URI uri) {
+ final Request req = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(30000, TimeUnit.MILLISECONDS);
+ req.header(HttpHeader.ACCEPT_LANGUAGE, "en"); // we want the on/off states in english
+ final String ah = this.authHeader;
+ if (ah != null) {
+ req.header(HttpHeader.AUTHORIZATION, ah);
+ }
+ return req;
+ }
+
+ protected PP parsePage(URI uri, PP pp)
+ throws ParseException, InterruptedException, TimeoutException, ExecutionException {
+ final ContentResponse response = prepareRequest(uri).send();
+
+ String responseString = null;
+ if (StringUtil.isBlank(response.getEncoding())) {
+ responseString = new String(response.getContent(), StandardCharsets.UTF_8);
+ } else {
+ responseString = response.getContentAsString();
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Response body was: {} ", responseString);
+ }
+
+ final ISimpleMarkupParser parser = new SimpleMarkupParser(this.noRestrictions);
+ parser.parse(responseString, pp);
+ return pp;
+ }
+
+ private void refreshData() {
+ URI schemaApiPage = this.schemaApiPage;
+ if (schemaApiPage == null) {
+ return;
+ }
+ try {
+ final ApiPageParser pp = parsePage(schemaApiPage,
+ new ApiPageParser(this, entries, this.channelTypeProvider));
+
+ if (pp.isConfigChanged()) {
+ // we have to update our channels...
+ final List channels = pp.getChannels();
+ final ThingBuilder thingBuilder = editThing();
+ thingBuilder.withChannels(channels);
+ updateThing(thingBuilder.build());
+ }
+ if (!this.online) {
+ updateStatus(ThingStatus.ONLINE);
+ this.online = true;
+ }
+ } catch (final InterruptedException e) {
+ // plugin shutdown is in progress
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
+ this.online = false;
+ } catch (final Exception e) {
+ logger.error("Error loading API Scheme: {} ", e.getMessage(), e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error: " + e.getMessage());
+ this.online = false;
+ }
+ }
+
+ @Override
+ public void handleCommand(final ChannelUID channelUID, final Command command) {
+ if (command instanceof RefreshType) {
+ // TODO how to debounce this? we could trigger refreshData() but during startup
+ // this issues lots of requests... :-/
+ return;
+ }
+ final ApiPageEntry e = this.entries.get(channelUID.getId());
+ if (e == null) {
+ logger.warn("Got command for unknown channel {}: {}", channelUID, command);
+ return;
+ }
+ final Request reqUpdate;
+ switch (e.type) {
+ case SwitchButton:
+ reqUpdate = prepareRequest(buildUri("INCLUDE/change.cgi?changeadrx2=" + e.address + "&changetox2="
+ + (command == OnOffType.ON ? "1" : "0")));
+ reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required...
+ break;
+ case SwitchForm:
+ ChangerX2Entry cx2e = e.changerX2Entry;
+ if (cx2e != null) {
+ reqUpdate = prepareRequest(buildUri("INCLUDE/change.cgi?changeadrx2=" + cx2e.address
+ + "&changetox2=" + (command == OnOffType.ON ? "1" : "0")));
+ reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required...
+ } else {
+ logger.warn("Got command for uninitalized channel {}: {}", channelUID, command);
+ return;
+ }
+ break;
+ case StateForm:
+ ChangerX2Entry cx2sf = e.changerX2Entry;
+ if (cx2sf != null) {
+ String val = cx2sf.options.get(((StringType) command).toFullString());
+ if (val != null) {
+ reqUpdate = prepareRequest(
+ buildUri("INCLUDE/change.cgi?changeadrx2=" + cx2sf.address + "&changetox2=" + val));
+ reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required...
+ } else {
+ logger.warn("Got unknown form command {} for channel {}; Valid commands are: {}", command,
+ channelUID, cx2sf.options.keySet());
+ return;
+ }
+ } else {
+ logger.warn("Got command for uninitalized channel {}: {}", channelUID, command);
+ return;
+ }
+ break;
+ case ReadOnlyNumeric:
+ case ReadOnlyState:
+ case ReadOnlySwitch:
+ logger.warn("Got command for ReadOnly channel {}: {}", channelUID, command);
+ return;
+ default:
+ logger.warn("Got command for unhandled type {} channel {}: {}", e.type, channelUID, command);
+ return;
+ }
+ try {
+ ContentResponse res = reqUpdate.send();
+ if (res.getStatus() == 200) {
+ // update ok, we update the state
+ updateState(channelUID, (State) command);
+ } else {
+ logger.error("Error sending update for {} = {}: {} {}", channelUID, command, res.getStatus(),
+ res.getReason());
+ }
+ } catch (InterruptedException | TimeoutException | ExecutionException ex) {
+ logger.error("Error sending update for {} = {}: {}", channelUID, command, ex.getMessage(), ex);
+ }
+ }
+
+ // make it accessible for ApiPageParser
+ @Override
+ protected void updateState(final ChannelUID channelUID, final State state) {
+ super.updateState(channelUID, state);
+ }
+
+ @Override
+ public void dispose() {
+ final ScheduledFuture> scheduledFuture = this.scheduledFuture;
+ if (scheduledFuture != null) {
+ try {
+ scheduledFuture.cancel(true);
+ this.scheduledFuture = null;
+ } catch (final Exception e) {
+ // swallow this
+ }
+ }
+ super.dispose();
+ }
+
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml
index b23a50bb32b98..156f03f6cedc9 100644
--- a/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml
@@ -103,4 +103,61 @@
+
+
+
+ Communication to a special "API" schema page on a "Technische Alternative C.M.I. Control and Monitoring Interface"
+
+
+
+
+ Hostname or IP address of the C.M.I.
+ network-address
+
+
+
+ Username for C.M.I. authentication
+
+
+
+ Password for C.M.I. authentication
+ password
+
+
+
+ ID of the schema API page
+
+
+
+ Poll intervall (in seconds) how often to poll the API Page
+ 10
+ true
+
+
+
+
+
+
+ Switch
+
+ An On/Off state read from C.M.I.
+
+
+
+ Switch
+
+ A modifiable On/Off state read from C.M.I.
+
+
+ Number
+
+ A numeric value read from C.M.I.
+
+
+
+ String
+
+ A state value read from C.M.I.
+
+