diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/InsightParser.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/InsightParser.java new file mode 100644 index 000000000..c80f74d7c --- /dev/null +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/InsightParser.java @@ -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 parse() { + HashMap 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); + } +} diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoInsightHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoInsightHandler.java index 02ca4ef6e..e028e7568 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoInsightHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoInsightHandler.java @@ -12,24 +12,17 @@ */ 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.TimeZone; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jdt.annotation.NonNullByDefault; 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.http.WemoHttpCall; 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.QuantityType; -import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.types.State; @@ -71,94 +64,21 @@ public class WemoInsightHandler extends WemoHandler { String insightParams = stateMap.get(variable); if (insightParams != null) { - String[] splitInsightParams = insightParams.split("\\|"); - - if (splitInsightParams[0] != null) { - OnOffType binaryState = "0".equals(splitInsightParams[0]) ? OnOffType.OFF : OnOffType.ON; - logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState, + InsightParser parser = new InsightParser(insightParams); + Map results = parser.parse(); + results.forEach((channel, state) -> { + logger.trace("New InsightParam {} '{}' for device '{}' received", channel, state, getThing().getUID()); - updateState(WemoBindingConstants.CHANNEL_STATE, binaryState); - } + updateState(channel, state); + }); - long lastChangedAt = 0; - try { - lastChangedAt = Long.parseLong(splitInsightParams[1]) * 1000; // convert s to ms - } catch (NumberFormatException e) { - logger.warn("Unable to parse lastChangedAt value '{}' for device '{}'; expected long", - splitInsightParams[1], getThing().getUID()); - } - 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); + // Update helper channel onStandBy by checking if currentPower > standByLimit. + var standByLimit = (QuantityType) results.get(WemoBindingConstants.CHANNEL_STANDBYLIMIT); + if (standByLimit != null) { + var currentPower = (QuantityType) results.get(WemoBindingConstants.CHANNEL_CURRENTPOWER); + if (currentPower != null) { + updateState(WemoBindingConstants.CHANNEL_ONSTANDBY, + OnOffType.from(currentPower.intValue() <= standByLimit.intValue())); } } } diff --git a/bundles/org.openhab.binding.wemo/src/test/java/org/openhab/binding/wemo/InsightParserTest.java b/bundles/org.openhab.binding.wemo/src/test/java/org/openhab/binding/wemo/InsightParserTest.java new file mode 100644 index 000000000..fe5c0dc6e --- /dev/null +++ b/bundles/org.openhab.binding.wemo/src/test/java/org/openhab/binding/wemo/InsightParserTest.java @@ -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 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 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 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 result = parser.parse(); + assertEquals(UnDefType.UNDEF, result.get(WemoBindingConstants.CHANNEL_LASTCHANGEDAT)); + } + + @Test + public void parseInvalidLastOnFor() { + InsightParser parser = new InsightParser("1|1645800647|A"); + Map result = parser.parse(); + assertEquals(UnDefType.UNDEF, result.get(WemoBindingConstants.CHANNEL_LASTONFOR)); + } +}