]> git.basschouten.com Git - openhab-addons.git/blob
7e10de7ace2d64e9d3710f9c8c46c37eea03b166
[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 decimalCommand) {
91             return positionEstimator.map(estimator -> estimator.processSetPosition(decimalCommand.intValue()))
92                     .orElseThrow(() -> {
93                         throw new IllegalArgumentException(
94                                 "Received position request but no estimation configured for channel " + channelId);
95                     });
96         }
97         int result = convertCommandToValue(channelId, command);
98         positionEstimator.ifPresent(PositionEstimator::cancelStopMovement);
99         return result;
100     }
101
102     @Override
103     protected State stateFromValue(String channelId, int value) {
104         if (value == 0x00) {
105             return OnOffType.OFF;
106         }
107         DirectionConfiguration configuration = getDirectionConfiguration(channelId);
108         if (value == configuration.up) {
109             return UpDownType.UP;
110         }
111         if (value == configuration.down) {
112             return UpDownType.DOWN;
113         }
114         throw new IllegalArgumentException("Unexpected value " + value + " received");
115     }
116
117     @Override
118     protected void updateState(ChannelUID channelUID, State state) {
119         logger.debug("updateState {} {}", channelUID, state);
120
121         getPositionEstimator(channelUID.getId()).ifPresentOrElse(estimator -> {
122             if (state == UpDownType.UP) {
123                 estimator.start(-1);
124             } else if (state == UpDownType.DOWN) {
125                 estimator.start(1);
126             } else if (state == OnOffType.OFF) {
127                 estimator.stop();
128             } else {
129                 logger.debug("Unexpected state update '{}' for '{}'", state, channelUID);
130             }
131         }, () -> super.updateState(channelUID, state));
132     }
133
134     private void updateState(ChannelUID channelUID, int percent) {
135         super.updateState(channelUID, new PercentType(percent));
136     }
137
138     protected int convertCommandToValue(String channelId, Command command) {
139         if (command == StopMoveType.STOP) {
140             return 0x00;
141         }
142         if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
143             return getDirectionConfiguration(channelId).down;
144         }
145         if (command == UpDownType.UP) {
146             return getDirectionConfiguration(channelId).up;
147         }
148         throw new IllegalArgumentException("Command '" + command + "' not supported");
149     }
150
151     private Optional<PositionEstimator> getPositionEstimator(String channelId) {
152         return positionEstimators.stream().filter(estimator -> channelId.equals(estimator.getChannelUID().getId()))
153                 .findFirst();
154     }
155
156     private DirectionConfiguration getDirectionConfiguration(String channelId) {
157         DirectionConfiguration configuration = directionConfigurations.get(channelId);
158         if (configuration == null) {
159             throw new IllegalArgumentException("Direction configuration not found for " + channelId);
160         }
161         return configuration;
162     }
163
164     public static class PositionEstimatorConfig {
165         public int duration = -1;
166         public int delay = 5;
167         public boolean reverse = false;
168     }
169
170     private class PositionEstimator {
171         private static final int updateIntervalInSec = 1;
172         private final ChannelUID channelUID;
173         private final int durationInMillis;
174         private final int delayInMillis;
175         private int position = 0;
176         private int turnOffMillis = 0;
177         private long startTimeMillis = 0;
178         private int direction = 0;
179         private @Nullable Future<?> updateEstimateFuture;
180         private @Nullable Future<?> stopMovementFuture;
181
182         PositionEstimator(ChannelUID channelUID, PositionEstimatorConfig config) {
183             this.channelUID = channelUID;
184
185             // Configuration is in seconds, but we operate with ms.
186             durationInMillis = config.duration * 1000;
187             delayInMillis = config.delay * 1000;
188         }
189
190         public ChannelUID getChannelUID() {
191             return channelUID;
192         }
193
194         public void destroy() {
195             Utils.cancel(updateEstimateFuture);
196             updateEstimateFuture = null;
197             cancelStopMovement();
198         }
199
200         public void start(int direction) {
201             stop();
202             synchronized (this) {
203                 this.direction = direction;
204                 turnOffMillis = delayInMillis + durationInMillis;
205                 startTimeMillis = System.currentTimeMillis();
206             }
207             updateEstimateFuture = scheduler.scheduleWithFixedDelay(() -> {
208                 updateEstimate();
209                 if (turnOffMillis <= 0) {
210                     handleCommand(channelUID, StopMoveType.STOP);
211                 }
212             }, updateIntervalInSec, updateIntervalInSec, TimeUnit.SECONDS);
213         }
214
215         public void stop() {
216             Utils.cancel(updateEstimateFuture);
217             updateEstimate();
218             synchronized (this) {
219                 this.direction = 0;
220                 startTimeMillis = 0;
221             }
222         }
223
224         public int processSetPosition(int percent) {
225             if (percent < 0 || percent > 100) {
226                 throw new IllegalArgumentException("Position % out of range - expecting [0, 100] but got " + percent
227                         + " for " + channelUID.getId());
228             }
229
230             cancelStopMovement();
231
232             int newPosition = (int) ((double) percent * (double) durationInMillis / 100.0 + 0.5);
233             int delta = position - newPosition;
234
235             logger.debug("Position set command {} for {}: delta = {}, current pos: {}, new position: {}", percent,
236                     channelUID, delta, position, newPosition);
237
238             if (delta == 0) {
239                 return convertCommandToValue(channelUID.getId(), StopMoveType.STOP);
240             }
241
242             int time = Math.abs(delta);
243             if (percent == 0 || percent == 100) {
244                 time += 5000; // Make sure we get to completely open/closed position.
245             }
246
247             stopMovementFuture = scheduler.schedule(() -> {
248                 handleCommand(channelUID, StopMoveType.STOP);
249             }, time, TimeUnit.MILLISECONDS);
250
251             return convertCommandToValue(channelUID.getId(), delta > 0 ? UpDownType.UP : UpDownType.DOWN);
252         }
253
254         public void cancelStopMovement() {
255             Utils.cancel(stopMovementFuture);
256             stopMovementFuture = null;
257         }
258
259         private void updateEstimate() {
260             int direction;
261             int ellapsedMillis;
262
263             synchronized (this) {
264                 direction = this.direction;
265                 if (startTimeMillis == 0) {
266                     ellapsedMillis = 0;
267                 } else {
268                     long currentTimeMillis = System.currentTimeMillis();
269                     ellapsedMillis = (int) (currentTimeMillis - startTimeMillis);
270                     startTimeMillis = currentTimeMillis;
271                 }
272             }
273
274             turnOffMillis -= ellapsedMillis;
275             position = Math.min(durationInMillis, Math.max(0, ellapsedMillis * direction + position));
276             int percent = (int) ((double) position / (double) durationInMillis * 100.0 + 0.5);
277
278             logger.debug(
279                     "Update estimate for '{}': position = {}, percent = {}, elapsed = {}ms, duration = {}ms, delay = {}ms, turnOff = {}ms",
280                     channelUID, position, percent, ellapsedMillis, durationInMillis, delayInMillis, turnOffMillis);
281
282             updateState(channelUID, percent);
283         }
284
285         @Override
286         public String toString() {
287             return "PositionEstimator('" + channelUID + "', duration = " + durationInMillis + "ms, delay = "
288                     + delayInMillis + "ms)";
289         }
290     }
291
292     private static class DirectionConfiguration {
293         final int up;
294         final int down;
295
296         static final DirectionConfiguration NORMAL = new DirectionConfiguration(1, 2);
297         static final DirectionConfiguration REVERSED = new DirectionConfiguration(2, 1);
298
299         private DirectionConfiguration(int up, int down) {
300             this.up = up;
301             this.down = down;
302         }
303     }
304 }