2 * Copyright (c) 2010-2021 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.concurrent.ConcurrentHashMap;
18 import java.util.concurrent.CopyOnWriteArrayList;
19 import java.util.concurrent.Future;
20 import java.util.concurrent.TimeUnit;
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;
39 * The {@link NikobusRollershutterModuleHandler} is responsible for communication between Nikobus
40 * rollershutter-controller and binding.
42 * @author Boris Krivonog - Initial contribution
45 public class NikobusRollershutterModuleHandler extends NikobusModuleHandler {
46 private final Logger logger = LoggerFactory.getLogger(NikobusRollershutterModuleHandler.class);
47 private final List<PositionEstimator> positionEstimators = new CopyOnWriteArrayList<>();
49 private final Map<String, DirectionConfiguration> directionConfigurations = new ConcurrentHashMap<>();
51 public NikobusRollershutterModuleHandler(Thing thing) {
56 public void initialize() {
59 if (thing.getStatus() == ThingStatus.OFFLINE) {
63 positionEstimators.clear();
64 directionConfigurations.clear();
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));
72 DirectionConfiguration configuration = config.reverse ? DirectionConfiguration.REVERSED
73 : DirectionConfiguration.NORMAL;
74 directionConfigurations.put(channel.getUID().getId(), configuration);
77 logger.debug("Position estimators for {} = {}", thing.getUID(), positionEstimators);
81 protected int valueFromCommand(String channelId, Command command) {
82 if (command == StopMoveType.STOP) {
85 if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
86 return getDirectionConfiguration(channelId).down;
88 if (command == UpDownType.UP) {
89 return getDirectionConfiguration(channelId).up;
91 throw new IllegalArgumentException("Command '" + command + "' not supported");
95 protected State stateFromValue(String channelId, int value) {
100 DirectionConfiguration configuration = getDirectionConfiguration(channelId);
101 if (value == configuration.up) {
102 return UpDownType.UP;
104 if (value == configuration.down) {
105 return UpDownType.DOWN;
108 throw new IllegalArgumentException("Unexpected value " + value + " received");
112 protected void updateState(ChannelUID channelUID, State state) {
113 logger.debug("updateState {} {}", channelUID, state);
115 positionEstimators.stream().filter(estimator -> channelUID.equals(estimator.getChannelUID())).findFirst()
116 .ifPresentOrElse(estimator -> {
117 if (state == UpDownType.UP) {
119 } else if (state == UpDownType.DOWN) {
121 } else if (state == OnOffType.OFF) {
124 logger.debug("Unexpected state update '{}' for '{}'", state, channelUID);
126 }, () -> super.updateState(channelUID, state));
129 private void updateState(ChannelUID channelUID, int percent) {
130 super.updateState(channelUID, new PercentType(percent));
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);
138 return configuration;
141 public static class PositionEstimatorConfig {
142 public int duration = -1;
143 public int delay = 5;
144 public boolean reverse = false;
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;
158 PositionEstimator(ChannelUID channelUID, PositionEstimatorConfig config) {
159 this.channelUID = channelUID;
161 // Configuration is in seconds, but we operate with ms.
162 durationInMillis = config.duration * 1000;
163 delayInMillis = config.delay * 1000;
166 public ChannelUID getChannelUID() {
170 public void start(int direction) {
172 synchronized (this) {
173 this.direction = direction;
174 turnOffMillis = delayInMillis + durationInMillis;
175 startTimeMillis = System.currentTimeMillis();
177 updateEstimateFuture = scheduler.scheduleWithFixedDelay(() -> {
179 if (turnOffMillis <= 0) {
180 handleCommand(channelUID, StopMoveType.STOP);
182 }, updateIntervalInSec, updateIntervalInSec, TimeUnit.SECONDS);
186 Utils.cancel(updateEstimateFuture);
188 synchronized (this) {
194 private void updateEstimate() {
198 synchronized (this) {
199 direction = this.direction;
200 if (startTimeMillis == 0) {
203 long currentTimeMillis = System.currentTimeMillis();
204 ellapsedMillis = (int) (currentTimeMillis - startTimeMillis);
205 startTimeMillis = currentTimeMillis;
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);
214 "Update estimate for '{}': position = {}, percent = {}, elapsed = {}ms, duration = {}ms, delay = {}ms, turnOff = {}ms",
215 channelUID, position, percent, ellapsedMillis, durationInMillis, delayInMillis, turnOffMillis);
217 updateState(channelUID, percent);
221 public String toString() {
222 return "PositionEstimator('" + channelUID + "', duration = " + durationInMillis + "ms, delay = "
223 + delayInMillis + "ms)";
227 private static class DirectionConfiguration {
231 final static DirectionConfiguration NORMAL = new DirectionConfiguration(1, 2);
232 final static DirectionConfiguration REVERSED = new DirectionConfiguration(2, 1);
234 private DirectionConfiguration(int up, int down) {