[wemo] Refactor Insight Switch parser (#12380)

* Extract Insight parser to separate class and provide unit tests

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2022-02-27 21:13:08 +01:00 committed by GitHub
parent 923f720a5c
commit d56b2e4dbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 284 additions and 94 deletions

View File

@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wemo.internal;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.unit.Units;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Parser for WeMo Insight Switch values.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class InsightParser {
private static final int INSIGHT_POSITION_STATE = 0;
private static final int INSIGHT_POSITION_LASTCHANGEDAT = 1;
private static final int INSIGHT_POSITION_LASTONFOR = 2;
private static final int INSIGHT_POSITION_ONTODAY = 3;
private static final int INSIGHT_POSITION_ONTOTAL = 4;
private static final int INSIGHT_POSITION_TIMESPAN = 5;
private static final int INSIGHT_POSITION_AVERAGEPOWER = 6;
private static final int INSIGHT_POSITION_CURRENTPOWER = 7;
private static final int INSIGHT_POSITION_ENERGYTODAY = 8;
private static final int INSIGHT_POSITION_ENERGYTOTAL = 9;
private static final int INSIGHT_POSITION_STANDBYLIMIT = 10;
private final String value;
public InsightParser(String value) {
this.value = value;
}
/**
* Parse provided string of values.
*
* @return Map of channel id's with states
*/
public Map<String, State> parse() {
HashMap<String, State> result = new HashMap<>();
String[] params = value.split("\\|");
for (int i = 0; i < params.length; i++) {
String value = params[i];
switch (i) {
case INSIGHT_POSITION_STATE:
result.put(WemoBindingConstants.CHANNEL_STATE, getOnOff(value));
break;
case INSIGHT_POSITION_LASTCHANGEDAT:
result.put(WemoBindingConstants.CHANNEL_LASTCHANGEDAT, getDateTime(value));
break;
case INSIGHT_POSITION_LASTONFOR:
result.put(WemoBindingConstants.CHANNEL_LASTONFOR, getNumber(value));
break;
case INSIGHT_POSITION_ONTODAY:
result.put(WemoBindingConstants.CHANNEL_ONTODAY, getNumber(value));
break;
case INSIGHT_POSITION_ONTOTAL:
result.put(WemoBindingConstants.CHANNEL_ONTOTAL, getNumber(value));
break;
case INSIGHT_POSITION_TIMESPAN:
result.put(WemoBindingConstants.CHANNEL_TIMESPAN, getNumber(value));
break;
case INSIGHT_POSITION_AVERAGEPOWER:
result.put(WemoBindingConstants.CHANNEL_AVERAGEPOWER, getPowerFromWatt(value));
break;
case INSIGHT_POSITION_CURRENTPOWER:
result.put(WemoBindingConstants.CHANNEL_CURRENTPOWER, getPowerFromMilliWatt(value));
break;
case INSIGHT_POSITION_ENERGYTODAY:
result.put(WemoBindingConstants.CHANNEL_ENERGYTODAY, getEnergy(value));
break;
case INSIGHT_POSITION_ENERGYTOTAL:
result.put(WemoBindingConstants.CHANNEL_ENERGYTOTAL, getEnergy(value));
break;
case INSIGHT_POSITION_STANDBYLIMIT:
result.put(WemoBindingConstants.CHANNEL_STANDBYLIMIT, getPowerFromMilliWatt(value));
break;
}
}
return result;
}
private State getOnOff(String value) {
return "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
}
private State getDateTime(String value) {
long lastChangedAt = 0;
try {
lastChangedAt = Long.parseLong(value);
} catch (NumberFormatException e) {
return UnDefType.UNDEF;
}
State lastChangedAtState = new DateTimeType(
ZonedDateTime.ofInstant(Instant.ofEpochSecond(lastChangedAt), ZoneId.systemDefault()));
if (lastChangedAt == 0) {
return UnDefType.UNDEF;
}
return lastChangedAtState;
}
private State getNumber(String value) {
try {
return DecimalType.valueOf(value);
} catch (NumberFormatException e) {
return UnDefType.UNDEF;
}
}
private State getPowerFromWatt(String value) {
return new QuantityType<>(DecimalType.valueOf(value), Units.WATT);
}
private State getPowerFromMilliWatt(String value) {
return new QuantityType<>(new BigDecimal(value).divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP),
Units.WATT);
}
private State getEnergy(String value) {
// recalculate mW-mins to Wh
return new QuantityType<>(new BigDecimal(value).divide(new BigDecimal(60_000), 0, RoundingMode.HALF_UP),
Units.WATT_HOUR);
}
}

View File

@ -12,24 +12,17 @@
*/ */
package org.openhab.binding.wemo.internal.handler; package org.openhab.binding.wemo.internal.handler;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Map; import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.wemo.internal.InsightParser;
import org.openhab.binding.wemo.internal.WemoBindingConstants; import org.openhab.binding.wemo.internal.WemoBindingConstants;
import org.openhab.binding.wemo.internal.http.WemoHttpCall; import org.openhab.binding.wemo.internal.http.WemoHttpCall;
import org.openhab.core.io.transport.upnp.UpnpIOService; import org.openhab.core.io.transport.upnp.UpnpIOService;
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.OnOffType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.State; import org.openhab.core.types.State;
@ -71,94 +64,21 @@ public class WemoInsightHandler extends WemoHandler {
String insightParams = stateMap.get(variable); String insightParams = stateMap.get(variable);
if (insightParams != null) { if (insightParams != null) {
String[] splitInsightParams = insightParams.split("\\|"); InsightParser parser = new InsightParser(insightParams);
Map<String, State> results = parser.parse();
if (splitInsightParams[0] != null) { results.forEach((channel, state) -> {
OnOffType binaryState = "0".equals(splitInsightParams[0]) ? OnOffType.OFF : OnOffType.ON; logger.trace("New InsightParam {} '{}' for device '{}' received", channel, state,
logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState,
getThing().getUID()); getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_STATE, binaryState); updateState(channel, state);
} });
long lastChangedAt = 0; // Update helper channel onStandBy by checking if currentPower > standByLimit.
try { var standByLimit = (QuantityType<?>) results.get(WemoBindingConstants.CHANNEL_STANDBYLIMIT);
lastChangedAt = Long.parseLong(splitInsightParams[1]) * 1000; // convert s to ms if (standByLimit != null) {
} catch (NumberFormatException e) { var currentPower = (QuantityType<?>) results.get(WemoBindingConstants.CHANNEL_CURRENTPOWER);
logger.warn("Unable to parse lastChangedAt value '{}' for device '{}'; expected long", if (currentPower != null) {
splitInsightParams[1], getThing().getUID()); updateState(WemoBindingConstants.CHANNEL_ONSTANDBY,
} OnOffType.from(currentPower.intValue() <= standByLimit.intValue()));
ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
TimeZone.getDefault().toZoneId());
State lastChangedAtState = new DateTimeType(zoned);
if (lastChangedAt != 0) {
logger.trace("New InsightParam lastChangedAt '{}' for device '{}' received", lastChangedAtState,
getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_LASTCHANGEDAT, lastChangedAtState);
}
State lastOnFor = DecimalType.valueOf(splitInsightParams[2]);
logger.trace("New InsightParam lastOnFor '{}' for device '{}' received", lastOnFor,
getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_LASTONFOR, lastOnFor);
State onToday = DecimalType.valueOf(splitInsightParams[3]);
logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday, getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_ONTODAY, onToday);
State onTotal = DecimalType.valueOf(splitInsightParams[4]);
logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal, getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_ONTOTAL, onTotal);
State timespan = DecimalType.valueOf(splitInsightParams[5]);
logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan, getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_TIMESPAN, timespan);
State averagePower = new QuantityType<>(DecimalType.valueOf(splitInsightParams[6]), Units.WATT); // natively
// given
// in W
logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_AVERAGEPOWER, averagePower);
BigDecimal currentMW = new BigDecimal(splitInsightParams[7]);
State currentPower = new QuantityType<>(currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP),
Units.WATT); // recalculate
// mW to W
logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_CURRENTPOWER, currentPower);
BigDecimal energyTodayMWMin = new BigDecimal(splitInsightParams[8]);
// recalculate mW-mins to Wh
State energyToday = new QuantityType<>(
energyTodayMWMin.divide(new BigDecimal(60000), 0, RoundingMode.HALF_UP), Units.WATT_HOUR);
logger.trace("New InsightParam energyToday '{}' for device '{}' received", energyToday,
getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_ENERGYTODAY, energyToday);
BigDecimal energyTotalMWMin = new BigDecimal(splitInsightParams[9]);
// recalculate mW-mins to Wh
State energyTotal = new QuantityType<>(
energyTotalMWMin.divide(new BigDecimal(60000), 0, RoundingMode.HALF_UP), Units.WATT_HOUR);
logger.trace("New InsightParam energyTotal '{}' for device '{}' received", energyTotal,
getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_ENERGYTOTAL, energyTotal);
if (splitInsightParams.length > 10 && splitInsightParams[10] != null) {
BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
State standByLimit = new QuantityType<>(
standByLimitMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP), Units.WATT); // recalculate
// mW to W
logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
getThing().getUID());
updateState(WemoBindingConstants.CHANNEL_STANDBYLIMIT, standByLimit);
if (currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue() > standByLimitMW
.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue()) {
updateState(WemoBindingConstants.CHANNEL_ONSTANDBY, OnOffType.OFF);
} else {
updateState(WemoBindingConstants.CHANNEL_ONSTANDBY, OnOffType.ON);
} }
} }
} }

View File

@ -0,0 +1,121 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wemo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.time.ZoneId;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.wemo.internal.InsightParser;
import org.openhab.binding.wemo.internal.WemoBindingConstants;
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.unit.Units;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Unit tests for {@link InsightParser}.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class InsightParserTest {
/**
* 'InsightParams' for subscription 'insight1'.
*/
@Test
public void parseUpnpInsightParams() {
InsightParser parser = new InsightParser(
"1|1645800647|109676|80323|1196960|1209600|44|41400|30288361|483361410|8000");
Map<String, State> result = parser.parse();
assertEquals(OnOffType.ON, result.get(WemoBindingConstants.CHANNEL_STATE));
assertEquals(DateTimeType.valueOf("2022-02-25T15:50:47.000+0100").toZone(ZoneId.systemDefault()),
result.get(WemoBindingConstants.CHANNEL_LASTCHANGEDAT));
assertEquals(new DecimalType(109_676), result.get(WemoBindingConstants.CHANNEL_LASTONFOR));
assertEquals(new DecimalType(80_323), result.get(WemoBindingConstants.CHANNEL_ONTODAY));
assertEquals(new DecimalType(1_196_960), result.get(WemoBindingConstants.CHANNEL_ONTOTAL));
assertEquals(new DecimalType(1_209_600), result.get(WemoBindingConstants.CHANNEL_TIMESPAN));
assertEquals(new QuantityType<>(44, Units.WATT), result.get(WemoBindingConstants.CHANNEL_AVERAGEPOWER));
assertEquals(new QuantityType<>(41, Units.WATT), result.get(WemoBindingConstants.CHANNEL_CURRENTPOWER));
assertEquals(new QuantityType<>(505, Units.WATT_HOUR), result.get(WemoBindingConstants.CHANNEL_ENERGYTODAY));
assertEquals(new QuantityType<>(8056, Units.WATT_HOUR), result.get(WemoBindingConstants.CHANNEL_ENERGYTOTAL));
assertEquals(new QuantityType<>(8, Units.WATT), result.get(WemoBindingConstants.CHANNEL_STANDBYLIMIT));
}
/**
* 'InsightParams' received from HTTP call. Format is a bit different: State can be non-binary,
* e.g. 8 for ON, and energy total is formatted with decimals.
*/
@Test
public void parseHttpInsightParams() {
InsightParser parser = new InsightParser("8|1645967627|0|0|0|1209600|13|0|0|0.000000|8000");
Map<String, State> result = parser.parse();
assertEquals(OnOffType.ON, result.get(WemoBindingConstants.CHANNEL_STATE));
assertEquals(DateTimeType.valueOf("2022-02-27T14:13:47.000+0100").toZone(ZoneId.systemDefault()),
result.get(WemoBindingConstants.CHANNEL_LASTCHANGEDAT));
assertEquals(new DecimalType(0), result.get(WemoBindingConstants.CHANNEL_LASTONFOR));
assertEquals(new DecimalType(0), result.get(WemoBindingConstants.CHANNEL_ONTODAY));
assertEquals(new DecimalType(0), result.get(WemoBindingConstants.CHANNEL_ONTOTAL));
assertEquals(new DecimalType(1_209_600), result.get(WemoBindingConstants.CHANNEL_TIMESPAN));
assertEquals(new QuantityType<>(13, Units.WATT), result.get(WemoBindingConstants.CHANNEL_AVERAGEPOWER));
assertEquals(new QuantityType<>(0, Units.WATT), result.get(WemoBindingConstants.CHANNEL_CURRENTPOWER));
assertEquals(new QuantityType<>(0, Units.WATT_HOUR), result.get(WemoBindingConstants.CHANNEL_ENERGYTODAY));
assertEquals(new QuantityType<>(0, Units.WATT_HOUR), result.get(WemoBindingConstants.CHANNEL_ENERGYTOTAL));
assertEquals(new QuantityType<>(8, Units.WATT), result.get(WemoBindingConstants.CHANNEL_STANDBYLIMIT));
}
/**
* Some devices provide 'BinaryState' for subscription 'basicevent1'. This contains
* the same information as 'InsightParams' except last parameter (stand-by limit).
*/
@Test
public void parseUpnpBinaryState() {
InsightParser parser = new InsightParser(
"1|1645800647|109676|80323|1196960|1209600|44|41400|30288361|483361410");
Map<String, State> result = parser.parse();
assertEquals(OnOffType.ON, result.get(WemoBindingConstants.CHANNEL_STATE));
assertEquals(DateTimeType.valueOf("2022-02-25T15:50:47.000+0100").toZone(ZoneId.systemDefault()),
result.get(WemoBindingConstants.CHANNEL_LASTCHANGEDAT));
assertEquals(new DecimalType(109_676), result.get(WemoBindingConstants.CHANNEL_LASTONFOR));
assertEquals(new DecimalType(80_323), result.get(WemoBindingConstants.CHANNEL_ONTODAY));
assertEquals(new DecimalType(1_196_960), result.get(WemoBindingConstants.CHANNEL_ONTOTAL));
assertEquals(new DecimalType(1_209_600), result.get(WemoBindingConstants.CHANNEL_TIMESPAN));
assertEquals(new QuantityType<>(44, Units.WATT), result.get(WemoBindingConstants.CHANNEL_AVERAGEPOWER));
assertEquals(new QuantityType<>(41, Units.WATT), result.get(WemoBindingConstants.CHANNEL_CURRENTPOWER));
assertEquals(new QuantityType<>(505, Units.WATT_HOUR), result.get(WemoBindingConstants.CHANNEL_ENERGYTODAY));
assertEquals(new QuantityType<>(8056, Units.WATT_HOUR), result.get(WemoBindingConstants.CHANNEL_ENERGYTOTAL));
assertNull(result.get(WemoBindingConstants.CHANNEL_STANDBYLIMIT));
}
@Test
public void parseInvalidLastChangedAt() {
InsightParser parser = new InsightParser("1|A");
Map<String, State> result = parser.parse();
assertEquals(UnDefType.UNDEF, result.get(WemoBindingConstants.CHANNEL_LASTCHANGEDAT));
}
@Test
public void parseInvalidLastOnFor() {
InsightParser parser = new InsightParser("1|1645800647|A");
Map<String, State> result = parser.parse();
assertEquals(UnDefType.UNDEF, result.get(WemoBindingConstants.CHANNEL_LASTONFOR));
}
}