]> git.basschouten.com Git - openhab-addons.git/blob
9637557638564a42aeeaa85faa3d3e733194307d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.concurrent.ConcurrentHashMap;
18 import java.util.concurrent.CopyOnWriteArrayList;
19 import java.util.concurrent.Future;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.nikobus.internal.utils.Utils;
25 import org.openhab.core.library.types.OnOffType;
26 import org.openhab.core.library.types.PercentType;
27 import org.openhab.core.library.types.StopMoveType;
28 import org.openhab.core.library.types.UpDownType;
29 import org.openhab.core.thing.Channel;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.types.Command;
34 import org.openhab.core.types.State;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * The {@link NikobusRollershutterModuleHandler} is responsible for communication between Nikobus
40  * rollershutter-controller and binding.
41  *
42  * @author Boris Krivonog - Initial contribution
43  */
44 @NonNullByDefault
45 public class NikobusRollershutterModuleHandler extends NikobusModuleHandler {
46     private final Logger logger = LoggerFactory.getLogger(NikobusRollershutterModuleHandler.class);
47     private final List<PositionEstimator> positionEstimators = new CopyOnWriteArrayList<>();
48
49     private final Map<String, DirectionConfiguration> directionConfigurations = new ConcurrentHashMap<>();
50
51     public NikobusRollershutterModuleHandler(Thing thing) {
52         super(thing);
53     }
54
55     @Override
56     public void initialize() {
57         super.initialize();
58
59         if (thing.getStatus() == ThingStatus.OFFLINE) {
60             return;
61         }
62
63         positionEstimators.clear();
64         directionConfigurations.clear();
65
66         for (Channel channel : thing.getChannels()) {
67             PositionEstimatorConfig config = channel.getConfiguration().as(PositionEstimatorConfig.class);
68             if (config.delay >= 0 && config.duration > 0) {
69                 positionEstimators.add(new PositionEstimator(channel.getUID(), config));
70             }
71
72             DirectionConfiguration configuration = config.reverse ? DirectionConfiguration.REVERSED
73                     : DirectionConfiguration.NORMAL;
74             directionConfigurations.put(channel.getUID().getId(), configuration);
75         }
76
77         logger.debug("Position estimators for {} = {}", thing.getUID(), positionEstimators);
78     }
79
80     @Override
81     protected int valueFromCommand(String channelId, Command command) {
82         if (command == StopMoveType.STOP) {
83             return 0x00;
84         }
85         if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
86             return getDirectionConfiguration(channelId).down;
87         }
88         if (command == UpDownType.UP) {
89             return getDirectionConfiguration(channelId).up;
90         }
91         throw new IllegalArgumentException("Command '" + command + "' not supported");
92     }
93
94     @Override
95     protected State stateFromValue(String channelId, int value) {
96         if (value == 0x00) {
97             return OnOffType.OFF;
98         }
99
100         DirectionConfiguration configuration = getDirectionConfiguration(channelId);
101         if (value == configuration.up) {
102             return UpDownType.UP;
103         }
104         if (value == configuration.down) {
105             return UpDownType.DOWN;
106         }
107
108         throw new IllegalArgumentException("Unexpected value " + value + " received");
109     }
110
111     @Override
112     protected void updateState(ChannelUID channelUID, State state) {
113         logger.debug("updateState {} {}", channelUID, state);
114
115         positionEstimators.stream().filter(estimator -> channelUID.equals(estimator.getChannelUID())).findFirst()
116                 .ifPresentOrElse(estimator -> {
117                     if (state == UpDownType.UP) {
118                         estimator.start(-1);
119                     } else if (state == UpDownType.DOWN) {
120                         estimator.start(1);
121                     } else if (state == OnOffType.OFF) {
122                         estimator.stop();
123                     } else {
124                         logger.debug("Unexpected state update '{}' for '{}'", state, channelUID);
125                     }
126                 }, () -> super.updateState(channelUID, state));
127     }
128
129     private void updateState(ChannelUID channelUID, int percent) {
130         super.updateState(channelUID, new PercentType(percent));
131     }
132
133     private DirectionConfiguration getDirectionConfiguration(String channelId) {
134         DirectionConfiguration configuration = directionConfigurations.get(channelId);
135         if (configuration == null) {
136             throw new IllegalArgumentException("Direction configuration not found for " + channelId);
137         }
138         return configuration;
139     }
140
141     public static class PositionEstimatorConfig {
142         public int duration = -1;
143         public int delay = 5;
144         public boolean reverse = false;
145     }
146
147     private class PositionEstimator {
148         private static final int updateIntervalInSec = 1;
149         private final ChannelUID channelUID;
150         private final int durationInMillis;
151         private final int delayInMillis;
152         private int position = 0;
153         private int turnOffMillis = 0;
154         private long startTimeMillis = 0;
155         private int direction = 0;
156         private @Nullable Future<?> updateEstimateFuture;
157
158         PositionEstimator(ChannelUID channelUID, PositionEstimatorConfig config) {
159             this.channelUID = channelUID;
160
161             // Configuration is in seconds, but we operate with ms.
162             durationInMillis = config.duration * 1000;
163             delayInMillis = config.delay * 1000;
164         }
165
166         public ChannelUID getChannelUID() {
167             return channelUID;
168         }
169
170         public void start(int direction) {
171             stop();
172             synchronized (this) {
173                 this.direction = direction;
174                 turnOffMillis = delayInMillis + durationInMillis;
175                 startTimeMillis = System.currentTimeMillis();
176             }
177             updateEstimateFuture = scheduler.scheduleWithFixedDelay(() -> {
178                 updateEstimate();
179                 if (turnOffMillis <= 0) {
180                     handleCommand(channelUID, StopMoveType.STOP);
181                 }
182             }, updateIntervalInSec, updateIntervalInSec, TimeUnit.SECONDS);
183         }
184
185         public void stop() {
186             Utils.cancel(updateEstimateFuture);
187             updateEstimate();
188             synchronized (this) {
189                 this.direction = 0;
190                 startTimeMillis = 0;
191             }
192         }
193
194         private void updateEstimate() {
195             int direction;
196             int ellapsedMillis;
197
198             synchronized (this) {
199                 direction = this.direction;
200                 if (startTimeMillis == 0) {
201                     ellapsedMillis = 0;
202                 } else {
203                     long currentTimeMillis = System.currentTimeMillis();
204                     ellapsedMillis = (int) (currentTimeMillis - startTimeMillis);
205                     startTimeMillis = currentTimeMillis;
206                 }
207             }
208
209             turnOffMillis -= ellapsedMillis;
210             position = Math.min(durationInMillis, Math.max(0, ellapsedMillis * direction + position));
211             int percent = (int) ((double) position / (double) durationInMillis * 100.0 + 0.5);
212
213             logger.debug(
214                     "Update estimate for '{}': position = {}, percent = {}, elapsed = {}ms, duration = {}ms, delay = {}ms, turnOff = {}ms",
215                     channelUID, position, percent, ellapsedMillis, durationInMillis, delayInMillis, turnOffMillis);
216
217             updateState(channelUID, percent);
218         }
219
220         @Override
221         public String toString() {
222             return "PositionEstimator('" + channelUID + "', duration = " + durationInMillis + "ms, delay = "
223                     + delayInMillis + "ms)";
224         }
225     }
226
227     private static class DirectionConfiguration {
228         final int up;
229         final int down;
230
231         final static DirectionConfiguration NORMAL = new DirectionConfiguration(1, 2);
232         final static DirectionConfiguration REVERSED = new DirectionConfiguration(2, 1);
233
234         private DirectionConfiguration(int up, int down) {
235             this.up = up;
236             this.down = down;
237         }
238     }
239 }