]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tacmi] Reworked unit-mapping between TA and OH; added support for timespans (#17556)
authorChristian Niessner <marvkis@users.noreply.github.com>
Mon, 21 Oct 2024 18:53:31 +0000 (20:53 +0200)
committerGitHub <noreply@github.com>
Mon, 21 Oct 2024 18:53:31 +0000 (20:53 +0200)
* [tacmi] Reworked unit-mapping between TA and OH; added support for timespans

Signed-off-by: Christian Niessner <github-marvkis@christian-niessner.de>
bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java
bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java
bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java
bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java
bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java
bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java
bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties
bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml

index 68a1a9adda25472780a03e51aa5286e896c88539..5e3bfe71002a9f7a68aad1f3b520b3b392bb29c6 100644 (file)
@@ -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,
index 01763c0b08ac2f01b47f55fb7246a8c0060afae2..65edccc7850155ec2c5eb94d0feaade1b85097b0 100644 (file)
@@ -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;
index d4b8d6175af1d47501ae6f9b6f99f31bb593e11b..fbd4aaab5383ce437f2132259d7ad284a8303c45 100644 (file)
@@ -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<String, ApiPageEntry> 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);
index 1b78d42119997ea39b4249fa12400938b644e4b4..87c664235fe7fa7871960ac97c07cbed61895cfc 100644 (file)
@@ -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,
index a084ed3d9909889fe21990c9c4f251f040267352..64db3c4e642390941d7d17465ba500fd55016ffa 100644 (file)
@@ -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);
                 }
index 8146151150e110d72c00acc3934880a2a30b6c8c..d0eff69cd5ec22f75a4738f9fe19f2a30904cfb5 100644 (file)
@@ -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<String, UnitAndType> 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:
index 30e4a4aaf4ce79073999f9820e8563e2814d6130..9ac3589e482aa84051703a85a18235ba45b93992 100644 (file)
@@ -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
index a3a8e3fada87d7299fb4cc0908574dd23d2d32d8..6a4277cbb9e737b88e0e2a038aa428d0f37de0b7 100644 (file)
                <state readOnly="true"/>
                <config-description-ref uri="channel-type:tacmi:schemaApiDefaults"/>
        </channel-type>
+       <channel-type id="schema-date-time-ro">
+               <item-type>DateTime</item-type>
+               <label>Time Value</label>
+               <description>A time read from C.M.I. - Only the time is supplied, the date part is set to the current day.</description>
+               <state readOnly="true"/>
+               <config-description-ref uri="channel-type:tacmi:schemaApiDefaults"/>
+       </channel-type>
 </thing:thing-descriptions>