]> git.basschouten.com Git - openhab-addons.git/blob
a33e43dd97e15ddf7f4d0566726e68687a687bf4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.nikobus.internal.handler;
14
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Optional;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.concurrent.CopyOnWriteArrayList;
20 import java.util.concurrent.Future;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.nikobus.internal.utils.Utils;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.PercentType;
29 import org.openhab.core.library.types.StopMoveType;
30 import org.openhab.core.library.types.UpDownType;
31 import org.openhab.core.thing.Channel;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.State;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * The {@link NikobusRollershutterModuleHandler} is responsible for communication between Nikobus
42  * rollershutter-controller and binding.
43  *
44  * @author Boris Krivonog - Initial contribution
45  */
46 @NonNullByDefault
47 public class NikobusRollershutterModuleHandler extends NikobusModuleHandler {
48     private final Logger logger = LoggerFactory.getLogger(NikobusRollershutterModuleHandler.class);
49     private final List<PositionEstimator> positionEstimators = new CopyOnWriteArrayList<>();
50     private final Map<String, DirectionConfiguration> directionConfigurations = new ConcurrentHashMap<>();
51
52     public NikobusRollershutterModuleHandler(Thing thing) {
53         super(thing);
54     }
55
56     @Override
57     public void initialize() {
58         super.initialize();
59
60         if (thing.getStatus() == ThingStatus.OFFLINE) {
61             return;
62         }
63
64         positionEstimators.clear();
65         directionConfigurations.clear();
66
67         for (Channel channel : thing.getChannels()) {
68             PositionEstimatorConfig config = channel.getConfiguration().as(PositionEstimatorConfig.class);
69             if (config.delay >= 0 && config.duration > 0) {
70                 positionEstimators.add(new PositionEstimator(channel.getUID(), config));
71             }
72
73             DirectionConfiguration configuration = config.reverse ? DirectionConfiguration.REVERSED
74                     : DirectionConfiguration.NORMAL;
75             directionConfigurations.put(channel.getUID().getId(), configuration);
76         }
77
78         logger.debug("Position estimators for {} = {}", thing.getUID(), positionEstimators);
79     }
80
81     @Override
82     public void dispose() {
83         positionEstimators.forEach(PositionEstimator::destroy);
84         super.dispose();
85     }
86
87     @Override
88     protected int valueFromCommand(String channelId, Command command) {
89         Optional<PositionEstimator> positionEstimator = getPositionEstimator(channelId);
90         if (command instanceof DecimalType) {
91             return positionEstimator.map(estimator -> {
92                 return estimator.processSetPosition(((DecimalType) command).intValue());
93             }).orElseThrow(() -> {
94                 throw new IllegalArgumentException(
95                         "Received position request but no estimation configured for channel " + channelId);
96             });
97         }
98         int result = convertCommandToValue(channelId, command);
99         positionEstimator.ifPresent(PositionEstimator::cancelStopMovement);
100         return result;
101     }
102
103     @Override
104     protected State stateFromValue(String channelId, int value) {
105         if (value == 0x00) {
106             return OnOffType.OFF;
107         }
108         DirectionConfiguration configuration = getDirectionConfiguration(channelId);
109         if (value == configuration.up) {
110             return UpDownType.UP;
111         }
112         if (value == configuration.down) {
113             return UpDownType.DOWN;
114         }
115         throw new IllegalArgumentException("Unexpected value " + value + " received");
116     }
117
118     @Override
119     protected void updateState(ChannelUID channelUID, State state) {
120         logger.debug("updateState {} {}", channelUID, state);
121
122         getPositionEstimator(channelUID.getId()).ifPresentOrElse(estimator -> {
123             if (state == UpDownType.UP) {
124                 estimator.start(-1);
125             } else if (state == UpDownType.DOWN) {
126                 estimator.start(1);
127             } else if (state == OnOffType.OFF) {
128                 estimator.stop();
129             } else {
130                 logger.debug("Unexpected state update '{}' for '{}'", state, channelUID);
131             }
132         }, () -> super.updateState(channelUID, state));
133     }
134
135     private void updateState(ChannelUID channelUID, int percent) {
136         super.updateState(channelUID, new PercentType(percent));
137     }
138
139     protected int convertCommandToValue(String channelId, Command command) {
140         if (command == StopMoveType.STOP) {
141             return 0x00;
142         }
143         if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
144             return getDirectionConfiguration(channelId).down;
145         }
146         if (command == UpDownType.UP) {
147             return getDirectionConfiguration(channelId).up;
148         }
149         throw new IllegalArgumentException("Command '" + command + "' not supported");
150     }
151
152     private Optional<PositionEstimator> getPositionEstimator(String channelId) {
153         return positionEstimators.stream().filter(estimator -> channelId.equals(estimator.getChannelUID().getId()))
154                 .findFirst();
155     }
156
157     private DirectionConfiguration getDirectionConfiguration(String channelId) {
158         DirectionConfiguration configuration = directionConfigurations.get(channelId);
159         if (configuration == null) {
160             throw new IllegalArgumentException("Direction configuration not found for " + channelId);
161         }
162         return configuration;
163     }
164
165     public static class PositionEstimatorConfig {
166         public int duration = -1;
167         public int delay = 5;
168         public boolean reverse = false;
169     }
170
171     private class PositionEstimator {
172         private static final int updateIntervalInSec = 1;
173         private final ChannelUID channelUID;
174         private final int durationInMillis;
175         private final int delayInMillis;
176         private int position = 0;
177         private int turnOffMillis = 0;
178         private long startTimeMillis = 0;
179         private int direction = 0;
180         private @Nullable Future<?> updateEstimateFuture;
181         private @Nullable Future<?> stopMovementFuture;
182
183         PositionEstimator(ChannelUID channelUID, PositionEstimatorConfig config) {
184             this.channelUID = channelUID;
185
186             // Configuration is in seconds, but we operate with ms.
187             durationInMillis = config.duration * 1000;
188             delayInMillis = config.delay * 1000;
189         }
190
191         public ChannelUID getChannelUID() {
192             return channelUID;
193         }
194
195         public void destroy() {
196             Utils.cancel(updateEstimateFuture);
197             updateEstimateFuture = null;
198             cancelStopMovement();
199         }
200
201         public void start(int direction) {
202             stop();
203             synchronized (this) {
204                 this.direction = direction;
205                 turnOffMillis = delayInMillis + durationInMillis;
206                 startTimeMillis = System.currentTimeMillis();
207             }
208             updateEstimateFuture = scheduler.scheduleWithFixedDelay(() -> {
209                 updateEstimate();
210                 if (turnOffMillis <= 0) {
211                     handleCommand(channelUID, StopMoveType.STOP);
212                 }
213             }, updateIntervalInSec, updateIntervalInSec, TimeUnit.SECONDS);
214         }
215
216         public void stop() {
217             Utils.cancel(updateEstimateFuture);
218             updateEstimate();
219             synchronized (this) {
220                 this.direction = 0;
221                 startTimeMillis = 0;
222             }
223         }
224
225         public int processSetPosition(int percent) {
226             if (percent < 0 || percent > 100) {
227                 throw new IllegalArgumentException("Position % out of range - expecting [0, 100] but got " + percent
228                         + " for " + channelUID.getId());
229             }
230
231             cancelStopMovement();
232
233             int newPosition = (int) ((double) percent * (double) durationInMillis / 100.0 + 0.5);
234             int delta = position - newPosition;
235
236             logger.debug("Position set command {} for {}: delta = {}, current pos: {}, new position: {}", percent,
237                     channelUID, delta, position, newPosition);
238
239             if (delta == 0) {
240                 return convertCommandToValue(channelUID.getId(), StopMoveType.STOP);
241             }
242
243             int time = Math.abs(delta);
244             if (percent == 0 || percent == 100) {
245                 time += 5000; // Make sure we get to completely open/closed position.
246             }
247
248             stopMovementFuture = scheduler.schedule(() -> {
249                 handleCommand(channelUID, StopMoveType.STOP);
250             }, time, TimeUnit.MILLISECONDS);
251
252             return convertCommandToValue(channelUID.getId(), delta > 0 ? UpDownType.UP : UpDownType.DOWN);
253         }
254
255         public void cancelStopMovement() {
256             Utils.cancel(stopMovementFuture);
257             stopMovementFuture = null;
258         }
259
260         private void updateEstimate() {
261             int direction;
262             int ellapsedMillis;
263
264             synchronized (this) {
265                 direction = this.direction;
266                 if (startTimeMillis == 0) {
267                     ellapsedMillis = 0;
268                 } else {
269                     long currentTimeMillis = System.currentTimeMillis();
270                     ellapsedMillis = (int) (currentTimeMillis - startTimeMillis);
271                     startTimeMillis = currentTimeMillis;
272                 }
273             }
274
275             turnOffMillis -= ellapsedMillis;
276             position = Math.min(durationInMillis, Math.max(0, ellapsedMillis * direction + position));
277             int percent = (int) ((double) position / (double) durationInMillis * 100.0 + 0.5);
278
279             logger.debug(
280                     "Update estimate for '{}': position = {}, percent = {}, elapsed = {}ms, duration = {}ms, delay = {}ms, turnOff = {}ms",
281                     channelUID, position, percent, ellapsedMillis, durationInMillis, delayInMillis, turnOffMillis);
282
283             updateState(channelUID, percent);
284         }
285
286         @Override
287         public String toString() {
288             return "PositionEstimator('" + channelUID + "', duration = " + durationInMillis + "ms, delay = "
289                     + delayInMillis + "ms)";
290         }
291     }
292
293     private static class DirectionConfiguration {
294         final int up;
295         final int down;
296
297         static final DirectionConfiguration NORMAL = new DirectionConfiguration(1, 2);
298         static final DirectionConfiguration REVERSED = new DirectionConfiguration(2, 1);
299
300         private DirectionConfiguration(int up, int down) {
301             this.up = up;
302             this.down = down;
303         }
304     }
305 }