Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tacmi] Reworked unit-mapping between TA and OH; added support for timespans #17556

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +35,8 @@ enum Type {
SWITCH_BUTTON(false),
SWITCH_FORM(false),
READ_ONLY_STATE(true),
STATE_FORM(false);
STATE_FORM(false),
TIME_PERIOD(false);

public final boolean readOnly;

Expand All @@ -52,6 +55,11 @@ private Type(boolean readOnly) {
*/
public final Channel channel;

/**
* Unit for this channel
*/
public final @Nullable Unit<?> unit;

/**
* internal address for this channel
*/
Expand All @@ -73,10 +81,11 @@ private Type(boolean readOnly) {
*/
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;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,41 +120,58 @@ public void handleOpenElement(final String elementName, final Map<String, String
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,24 @@
*/
package org.openhab.binding.tacmi.internal.schema;

import java.math.BigInteger;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
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 javax.measure.quantity.ElectricResistance;

import org.attoparser.ParseException;
import org.attoparser.config.ParseConfiguration;
import org.attoparser.config.ParseConfiguration.ElementBalancing;
Expand All @@ -41,7 +47,9 @@
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;
Expand All @@ -55,6 +63,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tech.units.indriya.AbstractUnit;
import tech.units.indriya.function.MultiplyConverter;
import tech.units.indriya.unit.TransformedUnit;

/**
* The {@link TACmiSchemaHandler} is responsible for handling commands, which are sent
* to one of the channels.
Expand All @@ -76,6 +88,20 @@ 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(AbstractUnit.ONE, "");
// marks an entry with special handling - i.e. 'Imp'
protected final UnitAndType SPECIALL_MARKER = new UnitAndType(AbstractUnit.ONE, "s");
// is an additional unit used by TA not provided / registered properly in the Units
protected final Unit<ElectricResistance> KILO_OHM = new TransformedUnit<>(Units.OHM,
MultiplyConverter.ofRational(BigInteger.valueOf(1000), BigInteger.ONE));

public TACmiSchemaHandler(final Thing thing, final HttpClient httpClient,
final TACmiChannelTypeProvider channelTypeProvider) {
super(thing);
Expand Down Expand Up @@ -256,7 +282,28 @@ public void handleCommand(final ChannelUID channelUID, final Command command) {
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...
Expand All @@ -273,6 +320,49 @@ public void handleCommand(final ChannelUID channelUID, final Command command) {
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>DateTime Value</label>
<description>A state value read from C.M.I.</description>
<state readOnly="true"/>
<config-description-ref uri="channel-type:tacmi:schemaApiDefaults"/>
</channel-type>
</thing:thing-descriptions>