2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nikobus.internal.handler;
15 import java.util.List;
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;
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;
41 * The {@link NikobusRollershutterModuleHandler} is responsible for communication between Nikobus
42 * rollershutter-controller and binding.
44 * @author Boris Krivonog - Initial contribution
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<>();
52 public NikobusRollershutterModuleHandler(Thing thing) {
57 public void initialize() {
60 if (thing.getStatus() == ThingStatus.OFFLINE) {
64 positionEstimators.clear();
65 directionConfigurations.clear();
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));
73 DirectionConfiguration configuration = config.reverse ? DirectionConfiguration.REVERSED
74 : DirectionConfiguration.NORMAL;
75 directionConfigurations.put(channel.getUID().getId(), configuration);
78 logger.debug("Position estimators for {} = {}", thing.getUID(), positionEstimators);
82 public void dispose() {
83 positionEstimators.forEach(PositionEstimator::destroy);
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);
98 int result = convertCommandToValue(channelId, command);
99 positionEstimator.ifPresent(PositionEstimator::cancelStopMovement);
104 protected State stateFromValue(String channelId, int value) {
106 return OnOffType.OFF;
108 DirectionConfiguration configuration = getDirectionConfiguration(channelId);
109 if (value == configuration.up) {
110 return UpDownType.UP;
112 if (value == configuration.down) {
113 return UpDownType.DOWN;
115 throw new IllegalArgumentException("Unexpected value " + value + " received");
119 protected void updateState(ChannelUID channelUID, State state) {
120 logger.debug("updateState {} {}", channelUID, state);
122 getPositionEstimator(channelUID.getId()).ifPresentOrElse(estimator -> {
123 if (state == UpDownType.UP) {
125 } else if (state == UpDownType.DOWN) {
127 } else if (state == OnOffType.OFF) {
130 logger.debug("Unexpected state update '{}' for '{}'", state, channelUID);
132 }, () -> super.updateState(channelUID, state));
135 private void updateState(ChannelUID channelUID, int percent) {
136 super.updateState(channelUID, new PercentType(percent));
139 protected int convertCommandToValue(String channelId, Command command) {
140 if (command == StopMoveType.STOP) {
143 if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
144 return getDirectionConfiguration(channelId).down;
146 if (command == UpDownType.UP) {
147 return getDirectionConfiguration(channelId).up;
149 throw new IllegalArgumentException("Command '" + command + "' not supported");
152 private Optional<PositionEstimator> getPositionEstimator(String channelId) {
153 return positionEstimators.stream().filter(estimator -> channelId.equals(estimator.getChannelUID().getId()))
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);
162 return configuration;
165 public static class PositionEstimatorConfig {
166 public int duration = -1;
167 public int delay = 5;
168 public boolean reverse = false;
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;
183 PositionEstimator(ChannelUID channelUID, PositionEstimatorConfig config) {
184 this.channelUID = channelUID;
186 // Configuration is in seconds, but we operate with ms.
187 durationInMillis = config.duration * 1000;
188 delayInMillis = config.delay * 1000;
191 public ChannelUID getChannelUID() {
195 public void destroy() {
196 Utils.cancel(updateEstimateFuture);
197 updateEstimateFuture = null;
198 cancelStopMovement();
201 public void start(int direction) {
203 synchronized (this) {
204 this.direction = direction;
205 turnOffMillis = delayInMillis + durationInMillis;
206 startTimeMillis = System.currentTimeMillis();
208 updateEstimateFuture = scheduler.scheduleWithFixedDelay(() -> {
210 if (turnOffMillis <= 0) {
211 handleCommand(channelUID, StopMoveType.STOP);
213 }, updateIntervalInSec, updateIntervalInSec, TimeUnit.SECONDS);
217 Utils.cancel(updateEstimateFuture);
219 synchronized (this) {
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());
231 cancelStopMovement();
233 int newPosition = (int) ((double) percent * (double) durationInMillis / 100.0 + 0.5);
234 int delta = position - newPosition;
236 logger.debug("Position set command {} for {}: delta = {}, current pos: {}, new position: {}", percent,
237 channelUID, delta, position, newPosition);
240 return convertCommandToValue(channelUID.getId(), StopMoveType.STOP);
243 int time = Math.abs(delta);
244 if (percent == 0 || percent == 100) {
245 time += 5000; // Make sure we get to completely open/closed position.
248 stopMovementFuture = scheduler.schedule(() -> {
249 handleCommand(channelUID, StopMoveType.STOP);
250 }, time, TimeUnit.MILLISECONDS);
252 return convertCommandToValue(channelUID.getId(), delta > 0 ? UpDownType.UP : UpDownType.DOWN);
255 public void cancelStopMovement() {
256 Utils.cancel(stopMovementFuture);
257 stopMovementFuture = null;
260 private void updateEstimate() {
264 synchronized (this) {
265 direction = this.direction;
266 if (startTimeMillis == 0) {
269 long currentTimeMillis = System.currentTimeMillis();
270 ellapsedMillis = (int) (currentTimeMillis - startTimeMillis);
271 startTimeMillis = currentTimeMillis;
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);
280 "Update estimate for '{}': position = {}, percent = {}, elapsed = {}ms, duration = {}ms, delay = {}ms, turnOff = {}ms",
281 channelUID, position, percent, ellapsedMillis, durationInMillis, delayInMillis, turnOffMillis);
283 updateState(channelUID, percent);
287 public String toString() {
288 return "PositionEstimator('" + channelUID + "', duration = " + durationInMillis + "ms, delay = "
289 + delayInMillis + "ms)";
293 private static class DirectionConfiguration {
297 static final DirectionConfiguration NORMAL = new DirectionConfiguration(1, 2);
298 static final DirectionConfiguration REVERSED = new DirectionConfiguration(2, 1);
300 private DirectionConfiguration(int up, int down) {