From ecbcd8202c8da230bca2e58db5bb1958afdcba49 Mon Sep 17 00:00:00 2001 From: Christian Niessner Date: Sun, 2 Aug 2020 15:57:05 +0200 Subject: [PATCH] [tacmi] initial WIP-Checkin of the "Schema API Page" variant Signed-off-by: Christian Niessner --- bundles/org.openhab.binding.tacmi/README.md | 2 +- bundles/org.openhab.binding.tacmi/pom.xml | 10 +- .../tacmi/internal/TACmiBindingConstants.java | 10 + .../internal/TACmiChannelTypeProvider.java | 56 ++ .../tacmi/internal/TACmiHandlerFactory.java | 22 +- .../tacmi/internal/schema/ApiPageEntry.java | 64 +++ .../tacmi/internal/schema/ApiPageParser.java | 481 ++++++++++++++++++ .../tacmi/internal/schema/ChangerX2Entry.java | 71 +++ .../internal/schema/ChangerX2Parser.java | 236 +++++++++ .../schema/TACmiSchemaConfiguration.java | 50 ++ .../internal/schema/TACmiSchemaHandler.java | 300 +++++++++++ .../resources/ESH-INF/thing/thing-types.xml | 57 +++ 12 files changed, 1355 insertions(+), 4 deletions(-) create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java 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. + +