[tacmi] Unit of Measurement fixes, added missing DateTime support (#17481)

* [tacmi] Use US Locale to format float numbers.

The German locale uses a comma as a separator for decimal numbers,
which means that the C.M.I. only uses the full number.

Signed-off-by: Christian Niessner <github-marvkis@christian-niessner.de>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Christian Niessner 2024-10-05 14:56:49 +02:00 committed by Ciprian Pascu
parent 3be45b9c8d
commit 172c124054
4 changed files with 124 additions and 26 deletions

View File

@ -15,6 +15,10 @@ package org.openhab.binding.tacmi.internal.schema;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -33,6 +37,7 @@ 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.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;
@ -196,7 +201,18 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler {
sb = sb.delete(0, 0);
}
if (this.fieldType == FieldType.READ_ONLY || this.fieldType == FieldType.FORM_VALUE) {
int len = sb.length();
int lids = sb.lastIndexOf(":");
if (len - lids == 3) {
int lids2 = sb.lastIndexOf(":", lids - 1);
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}")) {
lids = lids2;
}
}
}
int fsp = sb.indexOf(" ");
if (fsp < 0 || lids < 0 || fsp > lids) {
logger.debug("Invalid format for setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType,
@ -350,7 +366,7 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler {
// for the older pre-X2 devices (i.e. the UVR 1611) we get a comma. So we
// we replace all ',' with '.' to check if it's a valid number...
String val = valParts[0].replace(',', '.');
BigDecimal bd = new BigDecimal(val);
float bd = Float.parseFloat(val);
if (valParts.length == 2) {
if ("°C".equals(valParts[1])) {
channelType = "Number:Temperature";
@ -374,15 +390,14 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler {
state = new QuantityType<>(bd, Units.HERTZ);
} else if ("kW".equals(valParts[1])) {
channelType = "Number:Power";
bd = bd.multiply(new BigDecimal(1000));
bd = bd *= 1000;
state = new QuantityType<>(bd, Units.WATT);
} else if ("kWh".equals(valParts[1])) {
channelType = "Number:Power";
bd = bd.multiply(new BigDecimal(1000));
channelType = "Number:Energy";
state = new QuantityType<>(bd, Units.KILOWATT_HOUR);
} else if ("l/h".equals(valParts[1])) {
channelType = "Number:Volume";
bd = bd.divide(new BigDecimal(60));
channelType = "Number:VolumetricFlowRate";
bd = bd /= 60;
state = new QuantityType<>(bd, Units.LITRE_PER_MINUTE);
} else {
channelType = "Number";
@ -402,16 +417,27 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler {
type = Type.NUMERIC_FORM;
}
} catch (NumberFormatException nfe) {
// not a number...
channelType = "String";
ctuid = null;
// check for time....
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);
type = Type.NUMERIC_FORM;
} 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;
} else {
ctuid = null;
type = Type.STATE_FORM;
}
state = new StringType(vs);
}
}
break;
@ -440,7 +466,29 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler {
logger.warn("Error loading API Scheme: {} ", ex.getMessage());
}
}
if (channel == null || !Objects.equals(ctuid, channel.getChannelTypeUID())) {
if (e != null && !channelType.equals(e.channel.getAcceptedItemType())) {
// channel type has changed. we have to rebuild the channel.
this.channels.remove(channel);
channel = null;
}
if (channel != null && ctuid == null && cx2e != null) {
// custom channel type - check if it already exists and recreate when needed...
ChannelTypeUID curCtuid = channel.getChannelTypeUID();
if (curCtuid == null) {
// we have to re-create and re-register the channel uuid
logger.debug("Re-Registering channel type UUID for: {} ", shortName);
var ct = buildAndRegisterChannelType(shortName, type, cx2e);
var channelBuilder = ChannelBuilder.create(channel);
channelBuilder.withType(ct.getUID());
channel = channelBuilder.build(); // update channel
} else {
// check if channel uuid still exists and re-carete when needed
ChannelType ct = channelTypeProvider.getChannelType(curCtuid, null);
if (ct == null) {
buildAndRegisterChannelType(shortName, type, cx2e);
}
}
} else if (channel == null || !Objects.equals(ctuid, channel.getChannelTypeUID())) {
logger.debug("Creating / updating channel {} of type {} for '{}'", shortName, channelType, description);
this.configChanged = true;
ChannelUID channelUID = new ChannelUID(this.taCmiSchemaHandler.getThing().getUID(), shortName);
@ -456,15 +504,6 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler {
logger.warn("Error configurating channel for {}: channeltype cannot be determined!", shortName);
}
channel = channelBuilder.build(); // add configuration property...
} else if (ctuid == null && cx2e != null) {
// custom channel type - check if it already exists and recreate when needed...
ChannelTypeUID curCtuid = channel.getChannelTypeUID();
if (curCtuid != null) {
ChannelType ct = channelTypeProvider.getChannelType(curCtuid, null);
if (ct == null) {
buildAndRegisterChannelType(shortName, type, cx2e);
}
}
}
this.configChanged = true;
e = new ApiPageEntry(type, channel, address, cx2e, state);
@ -543,8 +582,11 @@ public class ApiPageParser extends AbstractSimpleMarkupHandler {
}
}
break;
case TIME:
itemType = "DateTime";
break;
default:
throw new IllegalStateException();
throw new IllegalStateException("Unhandled OptionType: " + cx2e.optionType);
}
ChannelTypeBuilder<?> ctb = ChannelTypeBuilder
.state(new ChannelTypeUID(TACmiBindingConstants.BINDING_ID, shortName), shortName, itemType)

View File

@ -33,6 +33,7 @@ public class ChangerX2Entry {
enum OptionType {
NUMBER,
SELECT,
TIME,
}
/**

View File

@ -111,7 +111,7 @@ public class ChangerX2Parser extends AbstractSimpleMarkupHandler {
String type = attributes.get("type");
if ("number".equals(type)) {
this.optionType = OptionType.NUMBER;
// we transfer the limits from the input elemnt...
// we transfer the limits from the input element...
this.options.put(ChangerX2Entry.NUMBER_MIN, attributes.get(ChangerX2Entry.NUMBER_MIN));
this.options.put(ChangerX2Entry.NUMBER_MAX, attributes.get(ChangerX2Entry.NUMBER_MAX));
this.options.put(ChangerX2Entry.NUMBER_STEP, attributes.get(ChangerX2Entry.NUMBER_STEP));
@ -120,6 +120,45 @@ public class ChangerX2Parser extends AbstractSimpleMarkupHandler {
col, attributes);
}
}
} else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT)
&& "input".equals(elementName) && "changetotimeh".equals(id)) {
this.parserState = ParserState.INPUT_DATA;
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);
}
;
} 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);
}
;
} else {
logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line,
col, attributes);
}
}
} else if (this.parserState == ParserState.SELECT && "option".equals(elementName)) {
this.parserState = ParserState.SELECT_OPTION;
this.optionType = OptionType.SELECT;
@ -136,6 +175,8 @@ public class ChangerX2Parser extends AbstractSimpleMarkupHandler {
throws ParseException {
if (this.parserState == ParserState.INPUT && "input".equals(elementName)) {
this.parserState = ParserState.INIT;
} else if (this.parserState == ParserState.INPUT_DATA && "input".equals(elementName)) {
this.parserState = ParserState.INPUT;
} else if (this.parserState == ParserState.SELECT && "select".equals(elementName)) {
this.parserState = ParserState.INIT;
} else if (this.parserState == ParserState.SELECT_OPTION && "option".equals(elementName)) {
@ -159,6 +200,8 @@ public class ChangerX2Parser extends AbstractSimpleMarkupHandler {
channelName, line, col, value, prev, id);
}
}
} else if (this.parserState == ParserState.INPUT && "span".equals(elementName)) {
// span's are ignored...
} else {
logger.debug("Error parsing options for {}: Unexpected CloseElement in {}:{}: {}", channelName, line, col,
elementName);

View File

@ -17,6 +17,7 @@ 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.ExecutionException;
import java.util.concurrent.ScheduledFuture;
@ -38,6 +39,7 @@ import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
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.StringType;
import org.openhab.core.thing.Channel;
@ -253,8 +255,18 @@ public class TACmiSchemaHandler extends BaseThingHandler {
case NUMERIC_FORM:
ChangerX2Entry cx2en = e.changerX2Entry;
if (cx2en != null) {
reqUpdate = prepareRequest(buildUri("INCLUDE/change.cgi?changeadrx2=" + cx2en.address
+ "&changetox2=" + command.format("%.2f")));
String val;
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...
var zdt = dtt.getZonedDateTime();
val = Integer.toString(zdt.getHour() * 60 + zdt.getMinute());
} else {
val = command.format("%.2f");
}
reqUpdate = prepareRequest(
buildUri("INCLUDE/change.cgi?changeadrx2=" + cx2en.address + "&changetox2=" + val));
reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required...
} else {
logger.debug("Got command for uninitalized channel {}: {}", channelUID, command);