From af0ac6e474df3e163f195d7444c6a0f61a9f6e4e Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 25 Sep 2022 11:29:26 +0200 Subject: [PATCH] [miele] Migrate start channel to full DateTime channel and add end channel (#13393) * Migrate start/finish channels to full DateTime channels * Unmark start and duration as advanced channels * Add end channel Signed-off-by: Jacob Laursen --- bundles/org.openhab.binding.miele/README.md | 4 + .../miele/internal/MieleBindingConstants.java | 5 + .../miele/internal/MieleHandlerFactory.java | 23 ++-- .../miele/internal/TimeStabilizer.java | 104 ++++++++++++++++++ .../handler/CoffeeMachineHandler.java | 6 +- .../internal/handler/DishWasherHandler.java | 7 +- .../handler/DishwasherChannelSelector.java | 31 +----- .../handler/FridgeFreezerHandler.java | 6 +- .../miele/internal/handler/FridgeHandler.java | 7 +- .../miele/internal/handler/HobHandler.java | 6 +- .../miele/internal/handler/HoodHandler.java | 7 +- .../handler/MieleApplianceHandler.java | 61 +++++++++- .../internal/handler/OvenChannelSelector.java | 31 +----- .../miele/internal/handler/OvenHandler.java | 7 +- .../handler/TumbleDryerChannelSelector.java | 31 +----- .../internal/handler/TumbleDryerHandler.java | 7 +- .../WashingMachineChannelSelector.java | 31 +----- .../handler/WashingMachineHandler.java | 6 +- .../resources/OH-INF/i18n/miele.properties | 10 +- .../resources/OH-INF/thing/channeltypes.xml | 14 ++- .../resources/OH-INF/thing/dishwasher.xml | 1 + .../src/main/resources/OH-INF/thing/oven.xml | 1 + .../resources/OH-INF/thing/tumbledryer.xml | 1 + .../resources/OH-INF/thing/washingmachine.xml | 1 + .../miele/internal/TimeStabilizerTest.java | 88 +++++++++++++++ 25 files changed, 344 insertions(+), 152 deletions(-) create mode 100644 bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/TimeStabilizer.java create mode 100644 bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/TimeStabilizerTest.java diff --git a/bundles/org.openhab.binding.miele/README.md b/bundles/org.openhab.binding.miele/README.md index b2fb5b4bf..2aa12ed7a 100644 --- a/bundles/org.openhab.binding.miele/README.md +++ b/bundles/org.openhab.binding.miele/README.md @@ -125,6 +125,7 @@ Channels available for each appliance type are listed below. | phase | String | Read | Current phase of the program running on the appliance | | rawPhase | Number | Read | Current phase of the program running on the appliance as raw number | | start | DateTime | Read | Programmed start time of the program | +| end | DateTime | Read | End time of the program (programmed or running) | | duration | DateTime | Read | Duration of the program running on the appliance | | elapsed | DateTime | Read | Time elapsed in the program running on the appliance | | finish | DateTime | Read | Time to finish the program running on the appliance | @@ -237,6 +238,7 @@ Channels available for each appliance type are listed below. | phase | String | Read | Current phase of the program running on the appliance | | rawPhase | Number | Read | Current phase of the program running on the appliance as raw number | | start | DateTime | Read | Programmed start time of the program | +| end | DateTime | Read | End time of the program (programmed or running) | | duration | DateTime | Read | Duration of the program running on the appliance | | elapsed | DateTime | Read | Time elapsed in the program running on the appliance | | finish | DateTime | Read | Time to finish the program running on the appliance | @@ -279,6 +281,7 @@ See oven. | phase | String | Read | Current phase of the program running on the appliance | | rawPhase | Number | Read | Current phase of the program running on the appliance as raw number | | start | DateTime | Read | Programmed start time of the program | +| end | DateTime | Read | End time of the program (programmed or running) | | duration | DateTime | Read | Duration of the program running on the appliance | | elapsed | DateTime | Read | Time elapsed in the program running on the appliance | | finish | DateTime | Read | Time to finish the program running on the appliance | @@ -343,6 +346,7 @@ See oven. | phase | String | Read | Current phase of the program running on the appliance | | rawPhase | Number | Read | Current phase of the program running on the appliance as raw number | | start | DateTime | Read | Programmed start time of the program | +| end | DateTime | Read | End time of the program (programmed or running) | | duration | DateTime | Read | Duration of the program running on the appliance | | elapsed | DateTime | Read | Time elapsed in the program running on the appliance | | finish | DateTime | Read | Time to finish the program running on the appliance | diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java index 594a7fe18..cf80527d1 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java @@ -42,6 +42,8 @@ public class MieleBindingConstants { public static final String PROGRAM_ID_PROPERTY_NAME = "programId"; public static final String PHASE_PROPERTY_NAME = "phase"; public static final String RAW_PHASE_PROPERTY_NAME = "rawPhase"; + public static final String START_TIME_PROPERTY_NAME = "startTime"; + public static final String FINISH_TIME_PROPERTY_NAME = "finishTime"; // Shared Channel ID's public static final String STATE_TEXT_CHANNEL_ID = "state"; @@ -53,6 +55,9 @@ public class MieleBindingConstants { public static final String SUPERCOOL_CHANNEL_ID = "supercool"; public static final String SUPERFREEZE_CHANNEL_ID = "superfreeze"; public static final String SWITCH_CHANNEL_ID = "switch"; + public static final String START_CHANNEL_ID = "start"; + public static final String END_CHANNEL_ID = "end"; + public static final String FINISH_CHANNEL_ID = "finish"; public static final String POWER_CONSUMPTION_CHANNEL_ID = "powerConsumption"; public static final String WATER_CONSUMPTION_CHANNEL_ID = "waterConsumption"; diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleHandlerFactory.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleHandlerFactory.java index 7b3ab8979..2e975d777 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleHandlerFactory.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleHandlerFactory.java @@ -39,6 +39,7 @@ import org.openhab.binding.miele.internal.handler.WashingMachineHandler; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; @@ -73,16 +74,18 @@ public class MieleHandlerFactory extends BaseThingHandlerFactory { private final HttpClient httpClient; private final TranslationProvider i18nProvider; private final LocaleProvider localeProvider; + private final TimeZoneProvider timeZoneProvider; private Map> discoveryServiceRegs = new HashMap<>(); @Activate public MieleHandlerFactory(@Reference final HttpClientFactory httpClientFactory, final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider, - ComponentContext componentContext) { + final @Reference TimeZoneProvider timeZoneProvider, ComponentContext componentContext) { this.httpClient = httpClientFactory.getCommonHttpClient(); this.i18nProvider = i18nProvider; this.localeProvider = localeProvider; + this.timeZoneProvider = timeZoneProvider; } @Override @@ -113,31 +116,31 @@ public class MieleHandlerFactory extends BaseThingHandlerFactory { return handler; } else if (MieleApplianceHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { if (thing.getThingTypeUID().equals(THING_TYPE_HOOD)) { - return new HoodHandler(thing, i18nProvider, localeProvider); + return new HoodHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_FRIDGEFREEZER)) { - return new FridgeFreezerHandler(thing, i18nProvider, localeProvider); + return new FridgeFreezerHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_FRIDGE)) { - return new FridgeHandler(thing, i18nProvider, localeProvider); + return new FridgeHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_OVEN)) { - return new OvenHandler(thing, i18nProvider, localeProvider); + return new OvenHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_HOB)) { - return new HobHandler(thing, i18nProvider, localeProvider); + return new HobHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_WASHINGMACHINE)) { - return new WashingMachineHandler(thing, i18nProvider, localeProvider); + return new WashingMachineHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_DRYER)) { - return new TumbleDryerHandler(thing, i18nProvider, localeProvider); + return new TumbleDryerHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_DISHWASHER)) { - return new DishWasherHandler(thing, i18nProvider, localeProvider); + return new DishWasherHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } if (thing.getThingTypeUID().equals(THING_TYPE_COFFEEMACHINE)) { - return new CoffeeMachineHandler(thing, i18nProvider, localeProvider); + return new CoffeeMachineHandler(thing, i18nProvider, localeProvider, timeZoneProvider); } } diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/TimeStabilizer.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/TimeStabilizer.java new file mode 100644 index 000000000..ee8955e60 --- /dev/null +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/TimeStabilizer.java @@ -0,0 +1,104 @@ +/** + * 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.miele.internal; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Deque; +import java.util.Iterator; +import java.util.concurrent.ConcurrentLinkedDeque; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * {@link TimeStabilizer} keeps a cache of historic timestamp values in order to + * provide moving average calculations to smooth out short-term fluctuations. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class TimeStabilizer { + + private final static int SLIDING_SECONDS = 300; + private final static int MAX_FLUCTUATION_SECONDS = 180; + + private final Deque cache = new ConcurrentLinkedDeque(); + + private class Item { + public Instant start; + public Instant end; + public Instant instant; + + public Item(Instant instant, Instant start, Instant end) { + this.start = start; + this.end = end; + this.instant = instant; + } + } + + public TimeStabilizer() { + } + + public void clear() { + cache.clear(); + } + + public Instant apply(Instant value) { + return this.apply(value, Instant.now()); + } + + public Instant apply(Instant value, Instant now) { + if (cache.isEmpty()) { + cache.add(new Item(value, now, now)); + return value; + } + + @Nullable + Item first = cache.getFirst(); + @Nullable + Item last = cache.getLast(); + last.end = now; + + Instant windowStart = now.minusSeconds(SLIDING_SECONDS); + Instant start = first.start; + Instant base = first.instant; + long weightedDiffFromBase = 0; + final Iterator it = cache.iterator(); + while (it.hasNext()) { + Item current = it.next(); + + if (current.end.isBefore(windowStart)) { + it.remove(); + continue; + } + if (current.start.isBefore(windowStart) && current.end.isAfter(windowStart)) { + // Truncate last item before sliding window. + start = current.start = windowStart; + } + long secs = current.start.until(current.end, ChronoUnit.SECONDS); + weightedDiffFromBase += base.until(current.instant, ChronoUnit.SECONDS) * secs; + } + + Instant average = base.plusSeconds(weightedDiffFromBase / start.until(now, ChronoUnit.SECONDS)); + if (value.isBefore(average.minusSeconds(MAX_FLUCTUATION_SECONDS)) + || value.isAfter(average.plusSeconds(MAX_FLUCTUATION_SECONDS))) { + cache.clear(); + average = value; + } + + cache.add(new Item(value, now, now)); + + return average; + } +} diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineHandler.java index 20d00bd17..e0b10adc2 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineHandler.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.miele.internal.api.dto.DeviceProperty; import org.openhab.binding.miele.internal.exceptions.MieleRpcException; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; @@ -42,8 +43,9 @@ public class CoffeeMachineHandler extends MieleApplianceHandler private final Logger logger = LoggerFactory.getLogger(FridgeHandler.class); - public FridgeHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) { - super(thing, i18nProvider, localeProvider, FridgeChannelSelector.class, MIELE_DEVICE_CLASS_FRIDGE); + public FridgeHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider, + TimeZoneProvider timeZoneProvider) { + super(thing, i18nProvider, localeProvider, timeZoneProvider, FridgeChannelSelector.class, + MIELE_DEVICE_CLASS_FRIDGE); } @Override diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobHandler.java index 86e621047..fff8885bc 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobHandler.java @@ -16,6 +16,7 @@ import static org.openhab.binding.miele.internal.MieleBindingConstants.MIELE_DEV import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -31,8 +32,9 @@ import org.openhab.core.types.Command; @NonNullByDefault public class HobHandler extends MieleApplianceHandler { - public HobHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) { - super(thing, i18nProvider, localeProvider, HobChannelSelector.class, MIELE_DEVICE_CLASS_HOB); + public HobHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider, + TimeZoneProvider timeZoneProvider) { + super(thing, i18nProvider, localeProvider, timeZoneProvider, HobChannelSelector.class, MIELE_DEVICE_CLASS_HOB); } @Override diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodHandler.java index 35491260e..66552eaae 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodHandler.java @@ -17,6 +17,7 @@ import static org.openhab.binding.miele.internal.MieleBindingConstants.MIELE_DEV import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.miele.internal.exceptions.MieleRpcException; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; @@ -41,8 +42,10 @@ public class HoodHandler extends MieleApplianceHandler { private final Logger logger = LoggerFactory.getLogger(HoodHandler.class); - public HoodHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) { - super(thing, i18nProvider, localeProvider, HoodChannelSelector.class, MIELE_DEVICE_CLASS_HOOD); + public HoodHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider, + TimeZoneProvider timeZoneProvider) { + super(thing, i18nProvider, localeProvider, timeZoneProvider, HoodChannelSelector.class, + MIELE_DEVICE_CLASS_HOOD); } @Override diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java index 99edb672d..daaba7146 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java @@ -14,6 +14,10 @@ package org.openhab.binding.miele.internal.handler; import static org.openhab.binding.miele.internal.MieleBindingConstants.*; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.IllformedLocaleException; import java.util.Locale; @@ -24,12 +28,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.miele.internal.DeviceUtil; import org.openhab.binding.miele.internal.MieleTranslationProvider; +import org.openhab.binding.miele.internal.TimeStabilizer; import org.openhab.binding.miele.internal.api.dto.DeviceClassObject; import org.openhab.binding.miele.internal.api.dto.DeviceMetaData; import org.openhab.binding.miele.internal.api.dto.DeviceProperty; import org.openhab.binding.miele.internal.api.dto.HomeDevice; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -43,6 +50,7 @@ import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,19 +87,25 @@ public abstract class MieleApplianceHandler & ApplianceChannel protected TranslationProvider i18nProvider; protected LocaleProvider localeProvider; protected MieleTranslationProvider translationProvider; + private TimeZoneProvider timeZoneProvider; + private TimeStabilizer startTimeStabilizer; + private TimeStabilizer finishTimeStabilizer; private Class selectorType; protected String modelID; protected Map metaDataCache = new HashMap<>(); public MieleApplianceHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider, - Class selectorType, String modelID) { + TimeZoneProvider timeZoneProvider, Class selectorType, String modelID) { super(thing); this.i18nProvider = i18nProvider; this.localeProvider = localeProvider; this.selectorType = selectorType; this.modelID = modelID; this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider); + this.timeZoneProvider = timeZoneProvider; + this.startTimeStabilizer = new TimeStabilizer(); + this.finishTimeStabilizer = new TimeStabilizer(); } public ApplianceChannelSelector getValueSelectorFromChannelID(String valueSelectorText) @@ -180,6 +194,8 @@ public abstract class MieleApplianceHandler & ApplianceChannel } applianceId = null; } + startTimeStabilizer.clear(); + finishTimeStabilizer.clear(); } @Override @@ -242,6 +258,7 @@ public abstract class MieleApplianceHandler & ApplianceChannel metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata); } + ThingUID thingUid = getThing().getUID(); if (EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) { if (!dp.Value.isEmpty()) { byte[] extendedStateBytes = DeviceUtil.stringToBytes(dp.Value); @@ -252,6 +269,13 @@ public abstract class MieleApplianceHandler & ApplianceChannel } } return; + } else if (START_TIME_PROPERTY_NAME.equals(dp.Name)) { + updateStateFromTime(new ChannelUID(thingUid, START_CHANNEL_ID), dp.Value, startTimeStabilizer); + return; + } else if (FINISH_TIME_PROPERTY_NAME.equals(dp.Name)) { + updateDurationState(new ChannelUID(thingUid, FINISH_CHANNEL_ID), dp.Value); + updateStateFromTime(new ChannelUID(thingUid, END_CHANNEL_ID), dp.Value, finishTimeStabilizer); + return; } ApplianceChannelSelector selector = null; @@ -265,7 +289,6 @@ public abstract class MieleApplianceHandler & ApplianceChannel if (selector != null) { String channelId = selector.getChannelID(); - ThingUID thingUid = getThing().getUID(); State state = selector.getState(dpValue, dmd, this.translationProvider); if (selector.isProperty()) { String value = state.toString(); @@ -289,6 +312,40 @@ public abstract class MieleApplianceHandler & ApplianceChannel updateState(channelUid, state); } + private void updateStateFromTime(ChannelUID channelUid, String value, TimeStabilizer stabilizer) { + try { + long minutesFromNow = Long.valueOf(value); + if (minutesFromNow > 0) { + Instant rawTime = Instant.now().truncatedTo(ChronoUnit.MINUTES).plusSeconds(minutesFromNow * 60); + ZonedDateTime correctedTime = stabilizer.apply(rawTime).atZone(timeZoneProvider.getTimeZone()); + ZonedDateTime truncatedTime = correctedTime.truncatedTo(ChronoUnit.MINUTES); + logger.trace("Update state of {} from {} -> '{}' -> '{}' to '{}'", channelUid, minutesFromNow, rawTime, + correctedTime, truncatedTime); + updateState(channelUid, new DateTimeType(truncatedTime)); + return; + } + } catch (NumberFormatException e) { + // Fall through. + } + updateState(channelUid, UnDefType.UNDEF); + stabilizer.clear(); + } + + private void updateDurationState(ChannelUID channelUid, String value) { + try { + long minutesFromNow = Long.valueOf(value); + if (minutesFromNow > 0) { + ZonedDateTime remaining = ZonedDateTime.ofInstant(Instant.ofEpochSecond(minutesFromNow * 60), + ZoneId.of("UTC")); + updateState(channelUid, new DateTimeType(remaining.withZoneSameLocal(timeZoneProvider.getTimeZone()))); + return; + } + } catch (NumberFormatException e) { + // Fall through. + } + updateState(channelUid, UnDefType.UNDEF); + } + protected void updateSwitchOnOffFromState(DeviceProperty dp) { if (!STATE_PROPERTY_NAME.equals(dp.Name)) { return; diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java index 982430127..998df2e9a 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java @@ -68,20 +68,8 @@ public enum OvenChannelSelector implements ApplianceChannelSelector { } }, PROGRAM_PHASE(RAW_PHASE_PROPERTY_NAME, PHASE_CHANNEL_ID, DecimalType.class, false), - START_TIME("startTime", "start", DateTimeType.class, false) { - @Override - public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { - Date date = new Date(); - SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT+0")); - try { - date.setTime(Long.valueOf(s) * 60000); - } catch (Exception e) { - date.setTime(0); - } - return getState(dateFormatter.format(date)); - } - }, + START_TIME("", START_CHANNEL_ID, DateTimeType.class, false), + END_TIME("", END_CHANNEL_ID, DateTimeType.class, false), DURATION("duration", "duration", DateTimeType.class, false) { @Override public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { @@ -110,20 +98,7 @@ public enum OvenChannelSelector implements ApplianceChannelSelector { return getState(dateFormatter.format(date)); } }, - FINISH_TIME("finishTime", "finish", DateTimeType.class, false) { - @Override - public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { - Date date = new Date(); - SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT+0")); - try { - date.setTime(Long.valueOf(s) * 60000); - } catch (Exception e) { - date.setTime(0); - } - return getState(dateFormatter.format(date)); - } - }, + FINISH_TIME("", FINISH_CHANNEL_ID, DateTimeType.class, false), TARGET_TEMP("targetTemperature", "target", QuantityType.class, false) { @Override public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenHandler.java index 6573b60ef..7454d2096 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenHandler.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.miele.internal.api.dto.DeviceProperty; import org.openhab.binding.miele.internal.exceptions.MieleRpcException; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; @@ -43,8 +44,10 @@ public class OvenHandler extends MieleApplianceHandler { private final Logger logger = LoggerFactory.getLogger(OvenHandler.class); - public OvenHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) { - super(thing, i18nProvider, localeProvider, OvenChannelSelector.class, MIELE_DEVICE_CLASS_OVEN); + public OvenHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider, + TimeZoneProvider timeZoneProvider) { + super(thing, i18nProvider, localeProvider, timeZoneProvider, OvenChannelSelector.class, + MIELE_DEVICE_CLASS_OVEN); } @Override diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java index bfb8d6b67..e762fd181 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java @@ -73,20 +73,8 @@ public enum TumbleDryerChannelSelector implements ApplianceChannelSelector { } }, PROGRAM_PHASE(RAW_PHASE_PROPERTY_NAME, PHASE_CHANNEL_ID, DecimalType.class, false), - START_TIME("startTime", "start", DateTimeType.class, false) { - @Override - public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { - Date date = new Date(); - SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT+0")); - try { - date.setTime(Long.valueOf(s) * 60000); - } catch (Exception e) { - date.setTime(0); - } - return getState(dateFormatter.format(date)); - } - }, + START_TIME("", START_CHANNEL_ID, DateTimeType.class, false), + END_TIME("", END_CHANNEL_ID, DateTimeType.class, false), DURATION("duration", "duration", DateTimeType.class, false) { @Override public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { @@ -115,20 +103,7 @@ public enum TumbleDryerChannelSelector implements ApplianceChannelSelector { return getState(dateFormatter.format(date)); } }, - FINISH_TIME("finishTime", "finish", DateTimeType.class, false) { - @Override - public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { - Date date = new Date(); - SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT+0")); - try { - date.setTime(Long.valueOf(s) * 60000); - } catch (Exception e) { - date.setTime(0); - } - return getState(dateFormatter.format(date)); - } - }, + FINISH_TIME("", FINISH_CHANNEL_ID, DateTimeType.class, false), DRYING_STEP("dryingStep", "step", DecimalType.class, false) { @Override public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerHandler.java index cfe8373d4..f47ea44db 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerHandler.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.miele.internal.api.dto.DeviceProperty; import org.openhab.binding.miele.internal.exceptions.MieleRpcException; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.ChannelUID; @@ -43,8 +44,10 @@ public class TumbleDryerHandler extends MieleApplianceHandler - + DateTime Programmed start time of the program Time - + - + + DateTime + + End time of the program (programmed or running) + Time + + + + DateTime Duration of the program running on the appliance diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml index dbcd85123..ae1f14bde 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml @@ -21,6 +21,7 @@ + diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml index 98dd1b5b7..f73629332 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml @@ -22,6 +22,7 @@ + diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml index 8655a1f36..17421f6df 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml @@ -22,6 +22,7 @@ + diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml index 79c69326b..967710bdb 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml @@ -22,6 +22,7 @@ + diff --git a/bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/TimeStabilizerTest.java b/bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/TimeStabilizerTest.java new file mode 100644 index 000000000..48222ea4d --- /dev/null +++ b/bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/TimeStabilizerTest.java @@ -0,0 +1,88 @@ +/** + * 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.miele.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link TimeStabilizer}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class TimeStabilizerTest { + + private @NonNullByDefault({}) TimeStabilizer stabilizer; + + @BeforeEach + public void initialize() { + stabilizer = new TimeStabilizer(); + } + + @Test + public void whenLongestPeriodIsFloorThenWeightedAverageIsLess() { + assertThat(stabilizer.apply(getInstantOf("02:00:00"), getInstantOf("22:00:00")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:01:00"), getInstantOf("22:00:31")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:00:00"), getInstantOf("22:01:00")), + is(equalTo(getInstantOf("02:00:29")))); + } + + @Test + public void whenLongestPeriodIsCeilThenWeightedAverageIsMore() { + assertThat(stabilizer.apply(getInstantOf("02:00:00"), getInstantOf("22:00:00")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:01:00"), getInstantOf("22:00:29")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:00:00"), getInstantOf("22:01:00")), + is(equalTo(getInstantOf("02:00:31")))); + } + + @Test + public void whenTooMuchFluctuationThenAverageIsDisregarded() { + assertThat(stabilizer.apply(getInstantOf("02:00:00"), getInstantOf("22:00:00")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:03:00"), getInstantOf("22:03:00")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:04:00"), getInstantOf("22:03:00")), + is(equalTo(getInstantOf("02:04:00")))); + } + + @Test + public void whenOutsideSlidingWindowThenValueIsDisregarded() { + assertThat(stabilizer.apply(getInstantOf("02:00:00"), getInstantOf("22:00:00")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:01:00"), getInstantOf("22:10:00")), + is(equalTo(getInstantOf("02:00:00")))); + assertThat(stabilizer.apply(getInstantOf("02:02:00"), getInstantOf("22:15:00")), + is(equalTo(getInstantOf("02:00:30")))); + assertThat(stabilizer.apply(getInstantOf("02:02:00"), getInstantOf("22:15:01")), + is(equalTo(getInstantOf("02:01:00")))); + } + + private Instant getInstantOf(String time) { + Clock clock = Clock.fixed(Instant.parse("2022-09-13T" + time + "Z"), ZoneId.of("UTC")); + return Instant.now(clock); + } +}