From 41d19067bfa8fa2d138850ff216906950ea7ed25 Mon Sep 17 00:00:00 2001 From: Christian Niessner Date: Mon, 21 Oct 2024 20:53:31 +0200 Subject: [PATCH] [tacmi] Reworked unit-mapping between TA and OH; added support for timespans (#17556) * [tacmi] Reworked unit-mapping between TA and OH; added support for timespans Signed-off-by: Christian Niessner Signed-off-by: Ciprian Pascu --- .../tacmi/internal/TACmiBindingConstants.java | 2 + .../tacmi/internal/schema/ApiPageEntry.java | 15 +- .../tacmi/internal/schema/ApiPageParser.java | 192 +++++++++++++----- .../tacmi/internal/schema/ChangerX2Entry.java | 1 + .../internal/schema/ChangerX2Parser.java | 71 ++++--- .../internal/schema/TACmiSchemaHandler.java | 83 +++++++- .../resources/OH-INF/i18n/tacmi.properties | 3 +- .../resources/OH-INF/thing/thing-types.xml | 7 + 8 files changed, 294 insertions(+), 80 deletions(-) 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 68a1a9adda2..5e3bfe71002 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 @@ -47,6 +47,8 @@ public class TACmiBindingConstants { "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_DATE_TIME_RO_UID = new ChannelTypeUID(BINDING_ID, + "schema-date-time-ro"); 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, 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 index 01763c0b08a..65edccc7850 100644 --- 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.tacmi.internal.schema; +import javax.measure.Unit; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.thing.Channel; @@ -33,7 +35,8 @@ public class ApiPageEntry { SWITCH_BUTTON(false), SWITCH_FORM(false), READ_ONLY_STATE(true), - STATE_FORM(false); + STATE_FORM(false), + TIME_PERIOD(false); public final boolean readOnly; @@ -52,6 +55,11 @@ public class ApiPageEntry { */ public final Channel channel; + /** + * Unit for this channel + */ + public final @Nullable Unit unit; + /** * internal address for this channel */ @@ -73,10 +81,11 @@ public class ApiPageEntry { */ private long lastCommandTS; - protected ApiPageEntry(final Type type, final Channel channel, @Nullable final String address, - @Nullable ChangerX2Entry changerX2Entry, State lastState) { + protected ApiPageEntry(final Type type, final Channel channel, @Nullable final Unit unit, + @Nullable final String address, @Nullable ChangerX2Entry changerX2Entry, State lastState) { this.type = type; this.channel = channel; + this.unit = unit; this.address = address; this.changerX2Entry = changerX2Entry; this.lastState = lastState; 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 index d4b8d6175af..fbd4aaab538 100644 --- 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 @@ -22,12 +22,16 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import java.util.regex.Pattern; + +import javax.measure.Unit; import org.attoparser.ParseException; import org.attoparser.simple.AbstractSimpleMarkupHandler; @@ -37,12 +41,13 @@ import org.eclipse.jetty.util.StringUtil; 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.openhab.binding.tacmi.internal.schema.TACmiSchemaHandler.UnitAndType; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; -import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; @@ -53,6 +58,7 @@ import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.State; import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.StateOption; +import org.openhab.core.types.util.UnitUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,7 +107,10 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { // Time stamp when status request was started. private final long statusRequestStartTS; private static @Nullable URI configDescriptionUriAPISchemaDefaults; + private final Pattern timePattern = Pattern.compile("[0-9]{2}:[0-9]{2}"); + private final Pattern durationPattern = Pattern.compile("([0-9\\.]{1,4}[dhms] ?)+"); + // needed for unit rewrite. it seems OHM is not registered as symbol in the units. public ApiPageParser(TACmiSchemaHandler taCmiSchemaHandler, Map entries, TACmiChannelTypeProvider channelTypeProvider) { super(); @@ -208,7 +217,7 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { if (lids2 > 0 && (lids - lids2 >= 3 && lids - lids2 <= 7)) { // the given value might be a time. validate it String timeCandidate = sb.substring(lids2 + 1).trim(); - if (timeCandidate.length() == 5 && timeCandidate.matches("[0-9]{2}:[0-9]{2}")) { + if (timeCandidate.length() == 5 && timePattern.matcher(timeCandidate).matches()) { lids = lids2; } } @@ -333,6 +342,7 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { return; // special state to indicate value currently cannot be retrieved.. } ApiPageEntry.Type type; + Unit unit; State state; String channelType; ChannelTypeUID ctuid; @@ -342,6 +352,7 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { state = OnOffType.from(this.buttonValue == ButtonValue.ON); ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RW_UID; channelType = "Switch"; + unit = null; break; case READ_ONLY: case FORM_VALUE: @@ -350,6 +361,7 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { if (isOn || "OFF".equals(vs) || "AUS".equals(vs)) { channelType = "Switch"; state = OnOffType.from(isOn); + unit = null; if (this.fieldType == FieldType.READ_ONLY || this.address == null) { ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RO_UID; type = Type.READ_ONLY_SWITCH; @@ -368,45 +380,70 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { String val = valParts[0].replace(',', '.'); float bd = Float.parseFloat(val); 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"; Number:Percent is currently not handled... - channelType = "Number:Dimensionless"; - state = new QuantityType<>(bd, Units.PERCENT); - } else if ("Imp".equals(valParts[1])) { - // impulses - no idea how to map this to something useful here? + var unitStr = valParts[1]; + var unitData = taCmiSchemaHandler.unitsCache.get(unitStr); + if (unitData == null) { + // we try to lookup the unit given by TA. + try { + // Special rewrite for electrical resistance measurements + // U+2126 is the 'real' OHM sign, but it seems to be registered as Greek Omega + // (U+03A9) in the units + String unitStrRepl = unitStr.replace((char) 0x2126, (char) 0x03A9); + // we build a 'normalized' value for parsing in QuantityType. + var qt = new QuantityType<>(val + " " + unitStrRepl, Locale.US); + // Just use the unit. We need to remember the unit in the channel data because we + // need to send data to the C.M.I. in the same unit + unit = qt.getUnit(); + channelType = "Number:" + UnitUtils.getDimensionName(unit); + unitData = new UnitAndType(unit, channelType); + } catch (IllegalArgumentException iae) { + // failed to get unit... + if ("Imp".equals(unitStr) || "€$".contains(unitStr)) { + // special case + unitData = taCmiSchemaHandler.SPECIAL_MARKER; + } else { + unitData = taCmiSchemaHandler.NULL_MARKER; + logger.warn( + "Unhandled UoM '{}' - seen on channel {} '{}'; Message from QuantityType: {}", + valParts[1], shortName, description, iae.getMessage()); + } + } + taCmiSchemaHandler.unitsCache.put(unitStr, unitData); + } + if (unitData == taCmiSchemaHandler.NULL_MARKER) { + // no UoM mappable - just send value channelType = "Number"; + unit = null; state = new DecimalType(bd); - } else if ("V".equals(valParts[1])) { - channelType = "Number:Voltage"; - state = new QuantityType<>(bd, Units.VOLT); - } else if ("A".equals(valParts[1])) { - channelType = "Number:Current"; - state = new QuantityType<>(bd, Units.AMPERE); - } else if ("Hz".equals(valParts[1])) { - channelType = "Number:Frequency"; - state = new QuantityType<>(bd, Units.HERTZ); - } else if ("kW".equals(valParts[1])) { - channelType = "Number:Power"; - bd = bd *= 1000; - state = new QuantityType<>(bd, Units.WATT); - } else if ("kWh".equals(valParts[1])) { - channelType = "Number:Energy"; - state = new QuantityType<>(bd, Units.KILOWATT_HOUR); - } else if ("l/h".equals(valParts[1])) { - channelType = "Number:VolumetricFlowRate"; - bd = bd /= 60; - state = new QuantityType<>(bd, Units.LITRE_PER_MINUTE); + } else if (unitData == taCmiSchemaHandler.SPECIAL_MARKER) { + // special handling for unknown UoM + if ("Imp".equals(unitStr)) { // Number of Pulses + // impulses - no idea how to map this to something useful here? + channelType = "Number"; + unit = null; + state = new DecimalType(bd); + } else if ("€$".contains(unitStr)) { // Currency's + var currency = "€".equals(valParts[1]) ? "EUR" : "USD"; + unit = CurrencyUnits.getInstance().getUnit(currency); + if (unit == null) { + logger.trace("Currency {} is unknown, falling back to DecimalType", currency); + state = new DecimalType(bd); + channelType = "Number:Dimensionless"; + } else { + state = new QuantityType<>(bd, unit); + channelType = "Number:" + UnitUtils.getDimensionName(unit); + } + } else { + throw new IllegalStateException("BUG: " + unitStr + " is not mapped!"); + } } else { - channelType = "Number"; - state = new DecimalType(bd); - logger.debug("Unhandled UoM for channel {} of type {} for '{}': {}", shortName, - channelType, description, valParts[1]); + channelType = unitData.channelType(); + unit = unitData.unit(); + state = new QuantityType<>(bd, unit); } } else { channelType = "Number"; + unit = null; state = new DecimalType(bd); } if (this.fieldType == FieldType.READ_ONLY || this.address == null) { @@ -418,25 +455,62 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { } } catch (NumberFormatException nfe) { ctuid = null; - // check for time.... + unit = null; + // check for time - 'Time' field String[] valParts = vs.split(":"); if (valParts.length == 2) { - channelType = "DateTime"; // convert it to zonedDateTime with today as date and the // default timezone. var zdt = LocalTime.parse(vs, DateTimeFormatter.ofPattern("HH:mm")).atDate(LocalDate.now()) .atZone(ZoneId.systemDefault()); state = new DateTimeType(zdt); + channelType = "DateTime"; type = Type.NUMERIC_FORM; + if (this.fieldType == FieldType.READ_ONLY || this.address == null) { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_DATE_TIME_RO_UID; + type = Type.READ_ONLY_NUMERIC; + } } else { - // not a number and not time... - channelType = "String"; - state = new StringType(vs); - type = Type.STATE_FORM; - } - if (this.fieldType == FieldType.READ_ONLY || this.address == null) { - ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_STATE_RO_UID; - type = Type.READ_ONLY_STATE; + // durations are a set of '000d 00h 00m 00.0s` fields + var durMatcher = durationPattern.matcher(vs); + if (durMatcher.matches()) { + // we have a duration + var parts = vs.split(" "); + float time = 0; + // sum up parts to a time + for (var timePart : parts) { + // last char is time unit, part before is time. + // for seconds it could be a fraction; + var pl = timePart.length(); + var tu = timePart.charAt(pl - 1); + var tv = Float.parseFloat(timePart.substring(0, pl - 1)); + + time += switch (tu) { + case 'd' -> tv * 86400; // days - 24h*60m*60s + case 'h' -> tv * 3600; // hours - 60m*60s + case 'm' -> tv * 60; // minutes - 60s + case 's' -> tv; // seconds - pass value + default -> throw new IllegalArgumentException( + "Unexpected time unit " + tu + " in " + vs); + }; + } + state = new QuantityType<>(time, Units.SECOND); + channelType = "Number:Time"; + type = Type.TIME_PERIOD; + if (this.fieldType == FieldType.READ_ONLY || this.address == null) { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID; + type = Type.READ_ONLY_NUMERIC; + } + } else { + // not a number and not time or duration + channelType = "String"; + state = new StringType(vs); + type = Type.STATE_FORM; + if (this.fieldType == FieldType.READ_ONLY || this.address == null) { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_STATE_RO_UID; + type = Type.READ_ONLY_STATE; + } + } } } } @@ -450,7 +524,8 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { } ApiPageEntry e = this.entries.get(shortName); boolean isNewEntry; - if (e == null || e.type != type || !channelType.equals(e.channel.getAcceptedItemType())) { + if (e == null || e.type != type || !channelType.equals(e.channel.getAcceptedItemType()) + || !Objects.equals(e.unit, unit)) { @Nullable Channel channel = this.taCmiSchemaHandler.getThing().getChannel(shortName); @Nullable @@ -465,6 +540,22 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { } catch (final TimeoutException | InterruptedException | ExecutionException ex) { logger.warn("Error loading API Scheme: {} ", ex.getMessage()); } + if (cx2e == null) { + // switch channel to readOnly + this.fieldType = FieldType.READ_ONLY; + if (type == Type.NUMERIC_FORM || type == Type.TIME_PERIOD) { + if ("DateTime".equals(channelType)) { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_DATE_TIME_RO_UID; + } else { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID; + } + type = Type.READ_ONLY_NUMERIC; + } else { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_STATE_RO_UID; + type = Type.READ_ONLY_STATE; + } + + } } if (e != null && !channelType.equals(e.channel.getAcceptedItemType())) { // channel type has changed. we have to rebuild the channel. @@ -506,7 +597,7 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { channel = channelBuilder.build(); // add configuration property... } this.configChanged = true; - e = new ApiPageEntry(type, channel, address, cx2e, state); + e = new ApiPageEntry(type, channel, unit, address, cx2e, state); this.entries.put(shortName, e); isNewEntry = true; } else { @@ -524,6 +615,7 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { // we do 'On-Fetch' update when channel is changeable, otherwise 'On-Change' switch (e.type) { case NUMERIC_FORM: + case TIME_PERIOD: case STATE_FORM: case SWITCH_BUTTON: case SWITCH_FORM: @@ -583,7 +675,11 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler { } break; case TIME: - itemType = "DateTime"; + if (type == Type.TIME_PERIOD) { + itemType = "Number"; + } else { + itemType = "DateTime"; + } break; default: throw new IllegalStateException("Unhandled OptionType: " + cx2e.optionType); 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 index 1b78d421199..87c664235fe 100644 --- 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 @@ -29,6 +29,7 @@ public class ChangerX2Entry { public static final String NUMBER_MIN = "min"; public static final String NUMBER_MAX = "max"; public static final String NUMBER_STEP = "step"; + public static final String TIME_PERIOD_PARTS = "timeParts"; enum OptionType { NUMBER, 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 index a084ed3d990..64db3c4e642 100644 --- 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 @@ -120,41 +120,58 @@ public class ChangerX2Parser extends AbstractSimpleMarkupHandler { col, attributes); } } - } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT) - && "input".equals(elementName) && "changetotimeh".equals(id)) { + } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT + || this.parserState == ParserState.INPUT_DATA) // input tags are not closed properly + && "input".equals(elementName) && id != null && id.startsWith("changetotime")) { this.parserState = ParserState.INPUT_DATA; + var timeType = id.charAt(12); if (attributes != null) { this.optionFieldName = attributes.get("name"); - String type = attributes.get("type"); if ("number".equals(attributes.get("type"))) { this.optionType = OptionType.TIME; - // validate hour limits - if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN)) - || !"24".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) { - logger.warn( - "Error parsing options for {}: Unexpected MIN/MAX values for hour input field in {}:{}: {}", - channelName, line, col, attributes); + switch (timeType) { + case 'h': + String maxHourValue = attributes.get(ChangerX2Entry.NUMBER_MAX); + // validate hour limits; for 'time' max is 24, for time period max is 23 ... + if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN)) + || (!"24".equals(maxHourValue) && !"23".equals(maxHourValue))) { + logger.warn( + "Error parsing options for {}: Unexpected MIN/MAX values for hour input field in {}:{}: {}", + channelName, line, col, attributes); + } + break; + case 'm': + case 's': + if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN)) + || !"59".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) { + logger.warn( + "Error parsing options for {}: Unexpected MIN/MAX values for minute input field in {}:{}: {}", + channelName, line, col, attributes); + } + break; + case 'z': // this is 'zehntelsekunde' - tenth of a second + if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN)) + || !"59.9".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) { + logger.warn( + "Error parsing options for {}: Unexpected MIN/MAX values for minute input field in {}:{}: {}", + channelName, line, col, attributes); + } + break; + case 'd': // for day's we don't validate. usually min = 0 and no max is given + break; + default: + throw new IllegalArgumentException( + "Unexpected timeType " + timeType + " during time span input field parsing"); } - ; - } else { - logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line, - col, attributes); - } - } - } else if ((this.parserState == ParserState.INPUT_DATA || this.parserState == ParserState.INPUT) - && "input".equals(elementName) && "changetotimem".equals(id)) { - this.parserState = ParserState.INPUT_DATA; - if (attributes != null) { - if ("number".equals(attributes.get("type"))) { - this.optionType = OptionType.TIME; - if (!"0".equals(attributes.get(ChangerX2Entry.NUMBER_MIN)) - || !"59".equals(attributes.get(ChangerX2Entry.NUMBER_MAX))) { - logger.warn( - "Error parsing options for {}: Unexpected MIN/MAX values for minute input field in {}:{}: {}", - channelName, line, col, attributes); + var timeParts = this.options.get(ChangerX2Entry.TIME_PERIOD_PARTS); + if (timeParts == null) { + timeParts = "" + timeType; + } else { + timeParts = timeParts + timeType; } - ; + this.options.put(ChangerX2Entry.TIME_PERIOD_PARTS, timeParts); } else { + logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line, col, attributes); } 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 index 8146151150e..d0eff69cd5e 100644 --- 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 @@ -19,11 +19,15 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import javax.measure.MetricPrefix; +import javax.measure.Unit; + import org.attoparser.ParseException; import org.attoparser.config.ParseConfiguration; import org.attoparser.config.ParseConfiguration.ElementBalancing; @@ -41,7 +45,9 @@ import org.eclipse.jetty.http.HttpMethod; import org.openhab.binding.tacmi.internal.TACmiChannelTypeProvider; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -76,6 +82,17 @@ public class TACmiSchemaHandler extends BaseThingHandler { private @Nullable ScheduledFuture scheduledFuture; private final ParseConfiguration noRestrictions; + // entry of the units lookup cache + record UnitAndType(Unit unit, String channelType) { + } + + // this is the units lookup cache. + protected final Map unitsCache = new ConcurrentHashMap<>(); + // marks an entry with known un-resolveable unit + protected final UnitAndType NULL_MARKER = new UnitAndType(Units.ONE, ""); + // marks an entry with special handling - i.e. 'Imp' + protected final UnitAndType SPECIAL_MARKER = new UnitAndType(Units.ONE, "s"); + public TACmiSchemaHandler(final Thing thing, final HttpClient httpClient, final TACmiChannelTypeProvider channelTypeProvider) { super(thing); @@ -256,7 +273,28 @@ public class TACmiSchemaHandler extends BaseThingHandler { ChangerX2Entry cx2en = e.changerX2Entry; if (cx2en != null) { String val; - if (command instanceof Number qt) { + if (command instanceof QuantityType qt) { + float value; + var taUnit = e.unit; + if (taUnit != null) { + // we try to convert to the unit TA expects for this channel + @SuppressWarnings("unchecked") + @Nullable + QuantityType qtConverted = qt.toUnit(taUnit); + if (qtConverted == null) { + logger.debug("Faild to convert unit {} to unit {} for command on channel {}", + qt.getUnit(), taUnit, channelUID); + value = qt.floatValue(); + } else { + value = qtConverted.floatValue(); + } + + } else { + // send raw value when there is no unit for this channel + value = qt.floatValue(); + } + val = String.format(Locale.US, "%.2f", value); + } else if (command instanceof Number qt) { val = String.format(Locale.US, "%.2f", qt.floatValue()); } else if (command instanceof DateTimeType dtt) { // time is transferred as minutes since midnight... @@ -273,6 +311,49 @@ public class TACmiSchemaHandler extends BaseThingHandler { return; } break; + case TIME_PERIOD: + ChangerX2Entry cx2enTime = e.changerX2Entry; + if (cx2enTime != null) { + long timeValMSec; + if (command instanceof QuantityType qt) { + @SuppressWarnings("unchecked") + QuantityType seconds = qt.toUnit(MetricPrefix.MILLI(Units.SECOND)); + if (seconds != null) { + timeValMSec = seconds.longValue(); + } else { + // fallback - assume we have a time in milliseconds + timeValMSec = qt.longValue(); + } + } else if (command instanceof Number qt) { + // fallback - assume we have a time in milliseconds + timeValMSec = qt.longValue(); + } else { + throw new IllegalArgumentException( + "Command " + command + " cannot be converted to a proper Timespan!"); + } + String val; + // TA has three different time periods. One is based on full seconds, the second on tenths of + // seconds and the third on minutes. We decide on the basis of the form fields provided during the + // ChangerX2 scan. + String parts = cx2enTime.options.get(ChangerX2Entry.TIME_PERIOD_PARTS); + if (parts == null || parts.indexOf('z') >= 0) { + // tenths of seconds + val = String.format(Locale.US, "%.1f", timeValMSec / 1000d); + } else if (parts.indexOf('s') >= 0) { + // seconds + val = String.format(Locale.US, "%d", timeValMSec / 1000); + } else { + // minutes + val = String.format(Locale.US, "%d", timeValMSec / 60000); + } + reqUpdate = prepareRequest( + buildUri("INCLUDE/change.cgi?changeadrx2=" + cx2enTime.address + "&changetox2=" + val)); + reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required... + } else { + logger.debug("Got command for uninitalized channel {}: {}", channelUID, command); + return; + } + break; case READ_ONLY_NUMERIC: case READ_ONLY_STATE: case READ_ONLY_SWITCH: diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties index 30e4a4aaf4c..9ac3589e482 100644 --- a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties +++ b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties @@ -28,7 +28,6 @@ thing-type.config.tacmi.cmiSchema.schemaId.label = API Schema Id thing-type.config.tacmi.cmiSchema.schemaId.description = ID of the schema API page thing-type.config.tacmi.cmiSchema.username.label = Username thing-type.config.tacmi.cmiSchema.username.description = Username for authentication on the C.M.I. - # channel types channel-type.tacmi.coe-analog-in.label = Analog Input Channel (C.M.I. -> OH) @@ -39,6 +38,8 @@ channel-type.tacmi.coe-digital-in.label = Digital Input (C.M.I. -> OH) channel-type.tacmi.coe-digital-in.description = A digital channel sent from C.M.I. to openHAB channel-type.tacmi.coe-digital-out.label = Digital Output (OH -> C.M.I.) channel-type.tacmi.coe-digital-out.description = A digital channel sent from OpenHAB to C.M.I. +channel-type.tacmi.schema-date-time-ro.label = Time Value +channel-type.tacmi.schema-date-time-ro.description = A time read from C.M.I. - Only the time is supplied, the date part is set to the current day. channel-type.tacmi.schema-numeric-ro.label = Value channel-type.tacmi.schema-numeric-ro.description = A numeric value read from C.M.I. channel-type.tacmi.schema-state-ro.label = Value diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml index a3a8e3fada8..6a4277cbb9e 100644 --- a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml @@ -173,4 +173,11 @@ + + DateTime + + A time read from C.M.I. - Only the time is supplied, the date part is set to the current day. + + +