]> git.basschouten.com Git - openhab-addons.git/commitdiff
[miele] Migrate start channel to full DateTime channel and add end channel (#13393)
authorJacob Laursen <jacob-github@vindvejr.dk>
Sun, 25 Sep 2022 09:29:26 +0000 (11:29 +0200)
committerGitHub <noreply@github.com>
Sun, 25 Sep 2022 09:29:26 +0000 (11:29 +0200)
* Migrate start/finish channels to full DateTime channels
* Unmark start and duration as advanced channels
* Add end channel

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
25 files changed:
bundles/org.openhab.binding.miele/README.md
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleHandlerFactory.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/TimeStabilizer.java [new file with mode: 0644]
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishWasherHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishwasherChannelSelector.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeFreezerHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerHandler.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java
bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineHandler.java
bundles/org.openhab.binding.miele/src/main/resources/OH-INF/i18n/miele.properties
bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/channeltypes.xml
bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml
bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/oven.xml
bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/tumbledryer.xml
bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml
bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/TimeStabilizerTest.java [new file with mode: 0644]

index b2fb5b4bff24edc985fa6783b1d2b2c300dcaa38..2aa12ed7a581114ae3c60ac4e672f436c90a03ec 100644 (file)
@@ -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                 |
index 594a7fe1869322348569e7c6ab93ccf66ae511c9..cf80527d17e19e13507722a8ce86a63dcba45bf6 100644 (file)
@@ -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";
 
index 7b3ab897947dc3ecd221f65106d3cc9d48e94b6e..2e975d7778252288429a9fdfda866ffc9bbebf55 100644 (file)
@@ -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<ThingUID, ServiceRegistration<?>> 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 (file)
index 0000000..ee8955e
--- /dev/null
@@ -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<Item> cache = new ConcurrentLinkedDeque<Item>();
+
+    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<Item> 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;
+    }
+}
index 20d00bd17ba8e2826013d7928d1de9f16564fc56..e0b10adc238f11f646a5f95d55fb5e381f2010dc 100644 (file)
@@ -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<CoffeeMachineCha
 
     private final Logger logger = LoggerFactory.getLogger(CoffeeMachineHandler.class);
 
-    public CoffeeMachineHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
-        super(thing, i18nProvider, localeProvider, CoffeeMachineChannelSelector.class,
+    public CoffeeMachineHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
+            TimeZoneProvider timeZoneProvider) {
+        super(thing, i18nProvider, localeProvider, timeZoneProvider, CoffeeMachineChannelSelector.class,
                 MIELE_DEVICE_CLASS_COFFEE_SYSTEM);
     }
 
index 4ad87b93d897654d9b83b22c26240b8b49a72139..8831bcc39a25b8e07990f35c4003b069217a79ec 100644 (file)
@@ -22,6 +22,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.library.types.QuantityType;
@@ -54,8 +55,10 @@ public class DishWasherHandler extends MieleApplianceHandler<DishwasherChannelSe
 
     private final Logger logger = LoggerFactory.getLogger(DishWasherHandler.class);
 
-    public DishWasherHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
-        super(thing, i18nProvider, localeProvider, DishwasherChannelSelector.class, MIELE_DEVICE_CLASS_DISHWASHER);
+    public DishWasherHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
+            TimeZoneProvider timeZoneProvider) {
+        super(thing, i18nProvider, localeProvider, timeZoneProvider, DishwasherChannelSelector.class,
+                MIELE_DEVICE_CLASS_DISHWASHER);
     }
 
     @Override
index 7c03a1bb198b3b4d14fdc04311d61475ec46fda3..78e132ecbb8ebd07aa9276400f09819f3fdc674f 100644 (file)
@@ -73,20 +73,8 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
         }
     },
     PROGRAM_PHASE(RAW_PHASE_PROPERTY_NAME, PHASE_CHANNEL_ID, DecimalType.class, false, false),
-    START_TIME("startTime", "start", DateTimeType.class, false, 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, false),
+    END_TIME("", END_CHANNEL_ID, DateTimeType.class, false, false),
     DURATION("duration", "duration", DateTimeType.class, false, false) {
         @Override
         public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) {
@@ -115,20 +103,7 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
             return getState(dateFormatter.format(date));
         }
     },
-    FINISH_TIME("finishTime", "finish", DateTimeType.class, false, 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, false),
     DOOR("signalDoor", "door", OpenClosedType.class, false, false) {
         @Override
         public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) {
index 1e2c56e25e3d65fde694b84318ad3a74aec9cacd..687303dc32922ffb7543f7609a5957bb4d220674 100644 (file)
@@ -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 FridgeFreezerHandler extends MieleApplianceHandler<FridgeFreezerCha
 
     private final Logger logger = LoggerFactory.getLogger(FridgeFreezerHandler.class);
 
-    public FridgeFreezerHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
-        super(thing, i18nProvider, localeProvider, FridgeFreezerChannelSelector.class,
+    public FridgeFreezerHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
+            TimeZoneProvider timeZoneProvider) {
+        super(thing, i18nProvider, localeProvider, timeZoneProvider, FridgeFreezerChannelSelector.class,
                 MIELE_DEVICE_CLASS_FRIDGE_FREEZER);
     }
 
index 840e89143eebfc65e84df3baa003f50fd1cbf74c..baf03769958c22c3a1d2d7cf7c3c4b90f440969c 100644 (file)
@@ -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 FridgeHandler extends MieleApplianceHandler<FridgeChannelSelector>
 
     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
index 86e62104759dd55fd6fce58e87c10a86a23d9994..fff8885bc769db382e58cfaab5e7f9db083641f6 100644 (file)
@@ -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<HobChannelSelector> {
 
-    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
index 35491260ea583575b2d3501f72f77924ce89f530..66552eaae68fc768495a99759c0ea23292fe9f2b 100644 (file)
@@ -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<HoodChannelSelector> {
 
     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
index 99edb672d519fe4d040105e11982ae9784c3ddbc..daaba7146744ebc51e0ab878973553459dc6c6a7 100644 (file)
@@ -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<E extends Enum<E> & ApplianceChannel
     protected TranslationProvider i18nProvider;
     protected LocaleProvider localeProvider;
     protected MieleTranslationProvider translationProvider;
+    private TimeZoneProvider timeZoneProvider;
+    private TimeStabilizer startTimeStabilizer;
+    private TimeStabilizer finishTimeStabilizer;
     private Class<E> selectorType;
     protected String modelID;
 
     protected Map<String, String> metaDataCache = new HashMap<>();
 
     public MieleApplianceHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
-            Class<E> selectorType, String modelID) {
+            TimeZoneProvider timeZoneProvider, Class<E> 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<E extends Enum<E> & ApplianceChannel
             }
             applianceId = null;
         }
+        startTimeStabilizer.clear();
+        finishTimeStabilizer.clear();
     }
 
     @Override
@@ -242,6 +258,7 @@ public abstract class MieleApplianceHandler<E extends Enum<E> & 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<E extends Enum<E> & 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<E extends Enum<E> & 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<E extends Enum<E> & 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;
index 9824301274129cf3d58613d80960ee2f2121e484..998df2e9a8f30b8ae3e6ec222f8913ec275f84bb 100644 (file)
@@ -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) {
index 6573b60efbe5b7fa90eedb87750f5daa71a94e7a..7454d20966b1e436541413e70647213f99885420 100644 (file)
@@ -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<OvenChannelSelector> {
 
     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
index bfb8d6b67f90514c58c308d31a716797fecc04ff..e762fd181eb181fce8c7211b925994ca091637c2 100644 (file)
@@ -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) {
index cfe8373d4759dfc1e25bad4504df9e553a88a556..f47ea44dbb3aa03dae79da24dd2602b0bfc5070a 100644 (file)
@@ -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<TumbleDryerChannel
 
     private final Logger logger = LoggerFactory.getLogger(TumbleDryerHandler.class);
 
-    public TumbleDryerHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
-        super(thing, i18nProvider, localeProvider, TumbleDryerChannelSelector.class, MIELE_DEVICE_CLASS_TUMBLE_DRYER);
+    public TumbleDryerHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
+            TimeZoneProvider timeZoneProvider) {
+        super(thing, i18nProvider, localeProvider, timeZoneProvider, TumbleDryerChannelSelector.class,
+                MIELE_DEVICE_CLASS_TUMBLE_DRYER);
     }
 
     @Override
index 65f24cddbf3e560b72ab6f19404a79cefc745433..44eecd47a8dcc10400b74a1c8a2a11eeb87c1084 100644 (file)
@@ -74,20 +74,8 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
         }
     },
     PROGRAM_PHASE(RAW_PHASE_PROPERTY_NAME, PHASE_CHANNEL_ID, DecimalType.class, false, false),
-    START_TIME("startTime", "start", DateTimeType.class, false, 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, false),
+    END_TIME("", END_CHANNEL_ID, DateTimeType.class, false, false),
     DURATION("duration", "duration", DateTimeType.class, false, false) {
         @Override
         public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) {
@@ -116,20 +104,7 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
             return getState(dateFormatter.format(date));
         }
     },
-    FINISH_TIME("finishTime", "finish", DateTimeType.class, false, 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, false),
     TARGET_TEMP("targetTemperature", "target", QuantityType.class, false, false) {
         @Override
         public State getState(String s, @Nullable DeviceMetaData dmd, MieleTranslationProvider translationProvider) {
index 649f3c56b699f01e744749a36966cde41ded61ae..6d3ae7be27a09dee668b533bcdf02f4ad7b8b914 100644 (file)
@@ -22,6 +22,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.library.types.QuantityType;
@@ -54,8 +55,9 @@ public class WashingMachineHandler extends MieleApplianceHandler<WashingMachineC
 
     private final Logger logger = LoggerFactory.getLogger(WashingMachineHandler.class);
 
-    public WashingMachineHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
-        super(thing, i18nProvider, localeProvider, WashingMachineChannelSelector.class,
+    public WashingMachineHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
+            TimeZoneProvider timeZoneProvider) {
+        super(thing, i18nProvider, localeProvider, timeZoneProvider, WashingMachineChannelSelector.class,
                 MIELE_DEVICE_CLASS_WASHING_MACHINE);
     }
 
index 2fc16547e37fe661b183798370a3f68950c30934..3517b67c60f2e3a2949f7348b5e6f017a369ac49 100644 (file)
@@ -68,6 +68,8 @@ channel-type.miele.duration.state.pattern = %1$tH:%1$tM
 channel-type.miele.elapsed.label = Elapsed Time
 channel-type.miele.elapsed.description = Time elapsed in the program running on the appliance
 channel-type.miele.elapsed.state.pattern = %1$tH:%1$tM
+channel-type.miele.end.label = End Time
+channel-type.miele.end.description = End time of the program (programmed or running)
 channel-type.miele.finish.label = Finish Time
 channel-type.miele.finish.description = Time to finish the program running on the appliance
 channel-type.miele.finish.state.pattern = %1$tH:%1$tM
@@ -97,7 +99,6 @@ channel-type.miele.spinningspeed.label = Spinning Speed
 channel-type.miele.spinningspeed.description = Spinning speed in the program running on the appliance
 channel-type.miele.start.label = Start Time
 channel-type.miele.start.description = Programmed start time of the program
-channel-type.miele.start.state.pattern = %1$tH:%1$tM
 channel-type.miele.state.label = State
 channel-type.miele.state.description = Current status of the appliance
 channel-type.miele.step.label = Step
@@ -132,7 +133,7 @@ offline.configuration-error.invalid-ip-multicast-interface = Invalid IP address
 offline.configuration-error.invalid-language = Invalid language: {0}
 offline.configuration-error.uid-not-set = Appliance ID is not set
 
-# Discovery result
+# discovery result
 
 discovery.xgw3000.label = Miele XGW 3000
 
@@ -170,7 +171,6 @@ miele.program.dishwasher.extra-quiet = Extra Quiet
 miele.program.dishwasher.hygiene = Hygiene
 miele.program.dishwasher.quickpowerwash = QuickPowerWash
 miele.program.dishwasher.tall-items = Tall items
-
 miele.program.tumbledryer.automatic-plus = Automatic Plus
 miele.program.tumbledryer.cottons = Cottons
 miele.program.tumbledryer.cottons-hygiene = Cottons hygiene
@@ -196,7 +196,6 @@ miele.program.tumbledryer.basket-programme = Basket programme
 miele.program.tumbledryer.smoothing = Smoothing
 miele.program.tumbledryer.cottons-auto-load-control = Cottons, auto load control
 miele.program.tumbledryer.minimum-iron-auto-load-control = Minimum iron, auto load control
-
 miele.program.washingmachine.cottons = Cottons
 miele.program.washingmachine.minimum-iron = Minimum iron
 miele.program.washingmachine.delicates = Delicates
@@ -233,7 +232,6 @@ miele.phase.dishwasher.rinses = Rinses
 miele.phase.dishwasher.final-rinse = Final rinse
 miele.phase.dishwasher.drying = Drying
 miele.phase.dishwasher.finished = Finished
-
 miele.phase.oven.heating = Heating
 miele.phase.oven.temp-hold = Temp. hold
 miele.phase.oven.door-open = Door Open
@@ -243,7 +241,6 @@ miele.phase.oven.searing-phase = Searing phase
 miele.phase.oven.defrost = Defrost
 miele.phase.oven.cooling-down = Cooling down
 miele.phase.oven.energy-save-phase = Energy save phase
-
 miele.phase.tumbledryer.programme-running = Programme running
 miele.phase.tumbledryer.drying = Drying
 miele.phase.tumbledryer.drying-machine-iron = Drying Machine iron
@@ -252,7 +249,6 @@ miele.phase.tumbledryer.drying-normal = Drying Normal
 miele.phase.tumbledryer.drying-normal-plus = Drying Normal+
 miele.phase.tumbledryer.cooling-down = Cooling down
 miele.phase.tumbledryer.finished = Finished
-
 miele.phase.washingmachine.pre-wash = Pre-wash
 miele.phase.washingmachine.washing = Washing
 miele.phase.washingmachine.rinses = Rinses
index 6d95eff040609974d4636a8286370b92305fc4cc..5f7c683ae58cdbfc4e6e5ef6dc903793f94498be 100644 (file)
                <state readOnly="true"></state>
        </channel-type>
 
-       <channel-type id="start" advanced="true">
+       <channel-type id="start">
                <item-type>DateTime</item-type>
                <label>Start Time</label>
                <description>Programmed start time of the program</description>
                <category>Time</category>
-               <state readOnly="true" pattern="%1$tH:%1$tM"></state>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="end">
+               <item-type>DateTime</item-type>
+               <label>End Time</label>
+               <description>End time of the program (programmed or running)</description>
+               <category>Time</category>
+               <state readOnly="true"></state>
        </channel-type>
 
-       <channel-type id="duration" advanced="true">
+       <channel-type id="duration">
                <item-type>DateTime</item-type>
                <label>Duration</label>
                <description>Duration of the program running on the appliance</description>
index dbcd85123999e680699ce5baed649ecb46f20442..ae1f14bde8e67bd6c75fbef5070e549e0bf4b14a 100644 (file)
@@ -21,6 +21,7 @@
                        <channel id="phase" typeId="phase"/>
                        <channel id="rawPhase" typeId="rawPhase"/>
                        <channel id="start" typeId="start"/>
+                       <channel id="end" typeId="end"/>
                        <channel id="duration" typeId="duration"/>
                        <channel id="elapsed" typeId="elapsed"/>
                        <channel id="finish" typeId="finish"/>
index 98dd1b5b7e0d5532ae948f14be33b073c1c0f2bb..f73629332b265b2a089c53f3a6072b0f4d3f382d 100644 (file)
@@ -22,6 +22,7 @@
                        <channel id="phase" typeId="phase"/>
                        <channel id="rawPhase" typeId="rawPhase"/>
                        <channel id="start" typeId="start"/>
+                       <channel id="end" typeId="end"/>
                        <channel id="duration" typeId="duration"/>
                        <channel id="elapsed" typeId="elapsed"/>
                        <channel id="finish" typeId="finish"/>
index 8655a1f3674d695f3abab33384a8dd20759df495..17421f6df47e48638c55b731f76cb01bd2de6b04 100644 (file)
@@ -22,6 +22,7 @@
                        <channel id="phase" typeId="phase"/>
                        <channel id="rawPhase" typeId="rawPhase"/>
                        <channel id="start" typeId="start"/>
+                       <channel id="end" typeId="end"/>
                        <channel id="duration" typeId="duration"/>
                        <channel id="elapsed" typeId="elapsed"/>
                        <channel id="finish" typeId="finish"/>
index 79c69326b565573317a5f7c8ed019b55192c2f81..967710bdb785966977829ef3fe1a8ab125dcd10f 100644 (file)
@@ -22,6 +22,7 @@
                        <channel id="phase" typeId="phase"/>
                        <channel id="rawPhase" typeId="rawPhase"/>
                        <channel id="start" typeId="start"/>
+                       <channel id="end" typeId="end"/>
                        <channel id="duration" typeId="duration"/>
                        <channel id="elapsed" typeId="elapsed"/>
                        <channel id="finish" typeId="finish"/>
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 (file)
index 0000000..48222ea
--- /dev/null
@@ -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);
+    }
+}