mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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 <github-marvkis@christian-niessner.de> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
749805d4a0
commit
41d19067bf
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -173,4 +173,11 @@
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user