]> git.basschouten.com Git - openhab-addons.git/commitdiff
[nikobus] rollershutter position estimator added (#9294)
authorBoris Krivonog <boris.krivonog@inova.si>
Fri, 11 Dec 2020 18:29:10 +0000 (19:29 +0100)
committerGitHub <noreply@github.com>
Fri, 11 Dec 2020 18:29:10 +0000 (10:29 -0800)
* Implemented rollershutter position estimator.
* Removed unneeded try-catch.

Signed-off-by: Boris Krivonog <boris.krivonog@inova.si>
bundles/org.openhab.binding.nikobus/README.md
bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusPushButtonHandler.java
bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusRollershutterModuleHandler.java
bundles/org.openhab.binding.nikobus/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.binding.nikobus/src/main/resources/OH-INF/thing/rollershutter-module.xml

index c5982f629d27e99f040fa92caafba60e5806f0e6..b75868e5ffec503d6de14f816b3d2021506d3241 100644 (file)
@@ -131,6 +131,24 @@ Defines a `rollershutter-module` with address `4C6C`.
 | output-5  | Rollershutter | Output 5     |
 | output-6  | Rollershutter | Output 6     |
 
+##### Estimating Position
+
+Nikobus rollershuter module does not provide information about rollershutter's position. In order to bridge this gap, an optional parameter `duration` can be set per channel, describing the amount of time needed by a rollershutter to get from open to closed state (or vice-versa).
+
+Binding uses this information to interpolate rollershutter’s position. On startup binding will assume completely open rollershutters but opening/closing a rollershutter once should bring it back in sync.
+
+After `duration` seconds elapsed, binding will set module's output back to neutral (OFF) state after additional number of seconds, as specified by the `delay` parameter. If not specified, it defaults to 5 seconds.
+
+Example:
+
+`duration = 30s`
+
+binding will automatically switch Nikobus rollershutter module’s output to OFF after
+
+`30s + 5s = 35s`
+
+**Note:** Please ensure all Nikobus Push Buttons manipulating rollershutters have `impactedModules` set so binding is notified about changes.
+
 ### Buttons
 
 Once an openHAB item has been configured as a Nikobus button, it will receive a status update to ON when the physical button is pressed.
index ef8e6653b31cd083246c356fe6e783497fc56434..27322eac03911c7fe4dbc001ccfc0b1a766d8341 100644 (file)
@@ -105,16 +105,11 @@ public class NikobusPushButtonHandler extends NikobusBaseThingHandler {
             logger.debug("Impacted modules for {} = {}", thing.getUID(), impactedModules);
         }
 
-        try {
-            for (Channel channel : thing.getChannels()) {
-                TriggerProcessor processor = createTriggerProcessor(channel);
-                if (processor != null) {
-                    triggerProcessors.add(processor);
-                }
+        for (Channel channel : thing.getChannels()) {
+            TriggerProcessor processor = createTriggerProcessor(channel);
+            if (processor != null) {
+                triggerProcessors.add(processor);
             }
-        } catch (RuntimeException e) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
-            return;
         }
 
         logger.debug("Trigger channels for {} = {}", thing.getUID(), triggerProcessors);
index 9d749f68e5bf1f28dccec2606d4f0f0841abcda1..60f70403a0b8ff18372317daddd3ae21ef6c2e6f 100644 (file)
  */
 package org.openhab.binding.nikobus.internal.handler;
 
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.nikobus.internal.utils.Utils;
 import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
 import org.openhab.core.library.types.StopMoveType;
 import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * The {@link NikobusRollershutterModuleHandler} is responsible for communication between Nikobus
@@ -28,10 +41,33 @@ import org.openhab.core.types.State;
  */
 @NonNullByDefault
 public class NikobusRollershutterModuleHandler extends NikobusModuleHandler {
+    private final Logger logger = LoggerFactory.getLogger(NikobusRollershutterModuleHandler.class);
+    private final List<PositionEstimator> positionEstimators = new CopyOnWriteArrayList<>();
+
     public NikobusRollershutterModuleHandler(Thing thing) {
         super(thing);
     }
 
+    @Override
+    public void initialize() {
+        super.initialize();
+
+        if (thing.getStatus() == ThingStatus.OFFLINE) {
+            return;
+        }
+
+        positionEstimators.clear();
+
+        for (Channel channel : thing.getChannels()) {
+            PositionEstimatorConfig config = channel.getConfiguration().as(PositionEstimatorConfig.class);
+            if (config.delay >= 0 && config.duration > 0) {
+                positionEstimators.add(new PositionEstimator(channel.getUID(), config));
+            }
+        }
+
+        logger.debug("Position estimators for {} = {}", thing.getUID(), positionEstimators);
+    }
+
     @Override
     protected int valueFromCommand(Command command) {
         if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
@@ -60,4 +96,111 @@ public class NikobusRollershutterModuleHandler extends NikobusModuleHandler {
         }
         throw new IllegalArgumentException("Unexpected value " + value + " received");
     }
+
+    @Override
+    protected void updateState(ChannelUID channelUID, State state) {
+        logger.debug("updateState {} {}", channelUID, state);
+
+        positionEstimators.stream().filter(estimator -> channelUID.equals(estimator.getChannelUID())).findFirst()
+                .ifPresentOrElse(estimator -> {
+                    if (state == UpDownType.UP) {
+                        estimator.start(-1);
+                    } else if (state == UpDownType.DOWN) {
+                        estimator.start(1);
+                    } else if (state == OnOffType.OFF) {
+                        estimator.stop();
+                    } else {
+                        logger.debug("Unexpected state update '{}' for '{}'", state, channelUID);
+                    }
+                }, () -> super.updateState(channelUID, state));
+    }
+
+    private void updateState(ChannelUID channelUID, int percent) {
+        super.updateState(channelUID, new PercentType(percent));
+    }
+
+    public static class PositionEstimatorConfig {
+        public int duration = -1;
+        public int delay = 5;
+    }
+
+    private class PositionEstimator {
+        private static final int updateIntervalInSec = 1;
+        private final ChannelUID channelUID;
+        private final int durationInMillis;
+        private final int delayInMillis;
+        private int position = 0;
+        private int turnOffMillis = 0;
+        private long startTimeMillis = 0;
+        private int direction = 0;
+        private @Nullable Future<?> updateEstimateFuture;
+
+        PositionEstimator(ChannelUID channelUID, PositionEstimatorConfig config) {
+            this.channelUID = channelUID;
+
+            // Configuration is in seconds, but we operate with ms.
+            durationInMillis = config.duration * 1000;
+            delayInMillis = config.delay * 1000;
+        }
+
+        public ChannelUID getChannelUID() {
+            return channelUID;
+        }
+
+        public void start(int direction) {
+            stop();
+            synchronized (this) {
+                this.direction = direction;
+                turnOffMillis = delayInMillis + durationInMillis;
+                startTimeMillis = System.currentTimeMillis();
+            }
+            updateEstimateFuture = scheduler.scheduleWithFixedDelay(() -> {
+                updateEstimate();
+                if (turnOffMillis <= 0) {
+                    handleCommand(channelUID, StopMoveType.STOP);
+                }
+            }, updateIntervalInSec, updateIntervalInSec, TimeUnit.SECONDS);
+        }
+
+        public void stop() {
+            Utils.cancel(updateEstimateFuture);
+            updateEstimate();
+            synchronized (this) {
+                this.direction = 0;
+                startTimeMillis = 0;
+            }
+        }
+
+        private void updateEstimate() {
+            int direction;
+            int ellapsedMillis;
+
+            synchronized (this) {
+                direction = this.direction;
+                if (startTimeMillis == 0) {
+                    ellapsedMillis = 0;
+                } else {
+                    long currentTimeMillis = System.currentTimeMillis();
+                    ellapsedMillis = (int) (currentTimeMillis - startTimeMillis);
+                    startTimeMillis = currentTimeMillis;
+                }
+            }
+
+            turnOffMillis -= ellapsedMillis;
+            position = Math.min(durationInMillis, Math.max(0, ellapsedMillis * direction + position));
+            int percent = (int) ((double) position / (double) durationInMillis * 100.0 + 0.5);
+
+            logger.debug(
+                    "Update estimate for '{}': position = {}, percent = {}, elapsed = {}ms, duration = {}ms, delay = {}ms, turnOff = {}ms",
+                    channelUID, position, percent, ellapsedMillis, durationInMillis, delayInMillis, turnOffMillis);
+
+            updateState(channelUID, percent);
+        }
+
+        @Override
+        public String toString() {
+            return "PositionEstimator('" + channelUID + "', duration = " + durationInMillis + "ms, delay = "
+                    + delayInMillis + "ms)";
+        }
+    }
 }
index 7418e61c504fda09bff7ac80a5c64c2ee1874b31..c7353d3e59d0fac9bf1cd9885fb21012888f3459 100644 (file)
                </parameter>
        </config-description>
 
+       <config-description uri="rollershutter-module:rollershutter-output:config">
+               <parameter name="duration" type="integer" min="1" unit="s">
+                       <label>Duration</label>
+                       <description>Duration in seconds required for a rollershutter to get from open to closed</description>
+               </parameter>
+               <parameter name="delay" type="integer" min="0" unit="s">
+                       <label>Delay</label>
+                       <description>Delay specifying how many seconds after duration module's output is set to OFF. Defaults to 5 seconds</description>
+               </parameter>
+       </config-description>
+
 </config-description:config-descriptions>
index 258f5cd55dd4e8a3efd54a8ad48125f981be0ec5..ee6ec5b65253a85dd61f80c1900f09a880f57557 100644 (file)
@@ -40,6 +40,7 @@
                <item-type>Rollershutter</item-type>
                <label>Output</label>
                <description>Rollershutter Module's Output</description>
+               <config-description-ref uri="rollershutter-module:rollershutter-output:config"/>
        </channel-type>
 
 </thing:thing-descriptions>