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 decimalCommand) {
91 return positionEstimator.map(estimator -> estimator.processSetPosition(decimalCommand.intValue()))
93 throw new IllegalArgumentException(
94 "Received position request but no estimation configured for channel " + channelId);
97 int result = convertCommandToValue(channelId, command);
98 positionEstimator.ifPresent(PositionEstimator::cancelStopMovement);
103 protected State stateFromValue(String channelId, int value) {
105 return OnOffType.OFF;
107 DirectionConfiguration configuration = getDirectionConfiguration(channelId);
108 if (value == configuration.up) {
109 return UpDownType.UP;
111 if (value == configuration.down) {
112 return UpDownType.DOWN;
114 throw new IllegalArgumentException("Unexpected value " + value + " received");
118 protected void updateState(ChannelUID channelUID, State state) {
119 logger.debug("updateState {} {}", channelUID, state);
121 getPositionEstimator(channelUID.getId()).ifPresentOrElse(estimator -> {
122 if (state == UpDownType.UP) {
124 } else if (state == UpDownType.DOWN) {
126 } else if (state == OnOffType.OFF) {
129 logger.debug("Unexpected state update '{}' for '{}'", state, channelUID);
131 }, () -> super.updateState(channelUID, state));
134 private void updateState(ChannelUID channelUID, int percent) {
135 super.updateState(channelUID, new PercentType(percent));
138 protected int convertCommandToValue(String channelId, Command command) {
139 if (command == StopMoveType.STOP) {
142 if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
143 return getDirectionConfiguration(channelId).down;
145 if (command == UpDownType.UP) {
146 return getDirectionConfiguration(channelId).up;
148 throw new IllegalArgumentException("Command '" + command + "' not supported");
151 private Optional<PositionEstimator> getPositionEstimator(String channelId) {
152 return positionEstimators.stream().filter(estimator -> channelId.equals(estimator.getChannelUID().getId()))
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);
161 return configuration;
164 public static class PositionEstimatorConfig {
165 public int duration = -1;
166 public int delay = 5;
167 public boolean reverse = false;
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;
182 PositionEstimator(ChannelUID channelUID, PositionEstimatorConfig config) {
183 this.channelUID = channelUID;
185 // Configuration is in seconds, but we operate with ms.
186 durationInMillis = config.duration * 1000;
187 delayInMillis = config.delay * 1000;
190 public ChannelUID getChannelUID() {
194 public void destroy() {
195 Utils.cancel(updateEstimateFuture);
196 updateEstimateFuture = null;
197 cancelStopMovement();
200 public void start(int direction) {
202 synchronized (this) {
203 this.direction = direction;
204 turnOffMillis = delayInMillis + durationInMillis;
205 startTimeMillis = System.currentTimeMillis();
207 updateEstimateFuture = scheduler.scheduleWithFixedDelay(() -> {
209 if (turnOffMillis <= 0) {
210 handleCommand(channelUID, StopMoveType.STOP);
212 }, updateIntervalInSec, updateIntervalInSec, TimeUnit.SECONDS);
216 Utils.cancel(updateEstimateFuture);
218 synchronized (this) {
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());
230 cancelStopMovement();
232 int newPosition = (int) ((double) percent * (double) durationInMillis / 100.0 + 0.5);
233 int delta = position - newPosition;
235 logger.debug("Position set command {} for {}: delta = {}, current pos: {}, new position: {}", percent,
236 channelUID, delta, position, newPosition);
239 return convertCommandToValue(channelUID.getId(), StopMoveType.STOP);
242 int time = Math.abs(delta);
243 if (percent == 0 || percent == 100) {
244 time += 5000; // Make sure we get to completely open/closed position.
247 stopMovementFuture = scheduler.schedule(() -> {
248 handleCommand(channelUID, StopMoveType.STOP);
249 }, time, TimeUnit.MILLISECONDS);
251 return convertCommandToValue(channelUID.getId(), delta > 0 ? UpDownType.UP : UpDownType.DOWN);
254 public void cancelStopMovement() {
255 Utils.cancel(stopMovementFuture);
256 stopMovementFuture = null;
259 private void updateEstimate() {
263 synchronized (this) {
264 direction = this.direction;
265 if (startTimeMillis == 0) {
268 long currentTimeMillis = System.currentTimeMillis();
269 ellapsedMillis = (int) (currentTimeMillis - startTimeMillis);
270 startTimeMillis = currentTimeMillis;
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);
279 "Update estimate for '{}': position = {}, percent = {}, elapsed = {}ms, duration = {}ms, delay = {}ms, turnOff = {}ms",
280 channelUID, position, percent, ellapsedMillis, durationInMillis, delayInMillis, turnOffMillis);
282 updateState(channelUID, percent);
286 public String toString() {
287 return "PositionEstimator('" + channelUID + "', duration = " + durationInMillis + "ms, delay = "
288 + delayInMillis + "ms)";
292 private static class DirectionConfiguration {
296 static final DirectionConfiguration NORMAL = new DirectionConfiguration(1, 2);
297 static final DirectionConfiguration REVERSED = new DirectionConfiguration(2, 1);
299 private DirectionConfiguration(int up, int down) {