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.transform.rollershutterposition.internal;
15 import static org.openhab.transform.rollershutterposition.internal.RollerShutterPositionConstants.*;
17 import java.time.Instant;
18 import java.time.temporal.ChronoUnit;
19 import java.util.Objects;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.common.ThreadPoolManager;
27 import org.openhab.core.library.types.PercentType;
28 import org.openhab.core.library.types.StopMoveType;
29 import org.openhab.core.library.types.UpDownType;
30 import org.openhab.core.thing.profiles.ProfileCallback;
31 import org.openhab.core.thing.profiles.ProfileContext;
32 import org.openhab.core.thing.profiles.ProfileTypeUID;
33 import org.openhab.core.thing.profiles.StateProfile;
34 import org.openhab.core.types.Command;
35 import org.openhab.core.types.State;
36 import org.openhab.transform.rollershutterposition.internal.config.RollerShutterPositionConfig;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * Profile to implement the RollerShutterPosition ItemChannelLink
43 * @author Jeff James - Initial contribution
45 * Core logic in this module has been heavily adapted from Tarag Gautier js script implementation
49 public class RollerShutterPositionProfile implements StateProfile {
50 private static final String PROFILE_THREADPOOL_NAME = "profile-rollershutterposition";
51 private final Logger logger = LoggerFactory.getLogger(RollerShutterPositionProfile.class);
53 private final ProfileCallback callback;
54 RollerShutterPositionConfig configuration;
56 private int position = 0; // current position of the roller shutter (assumes 0 when system starts)
57 private int targetPosition;
58 private boolean isValidConfiguration = false;
59 private Instant movingSince = Instant.MIN;
60 private UpDownType direction = UpDownType.DOWN;
62 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(PROFILE_THREADPOOL_NAME);
63 protected @Nullable ScheduledFuture<?> stopTimer = null;
64 protected @Nullable ScheduledFuture<?> updateTimer = null;
66 public RollerShutterPositionProfile(final ProfileCallback callback, final ProfileContext context) {
67 this.callback = callback;
68 this.configuration = context.getConfiguration().as(RollerShutterPositionConfig.class);
70 if (configuration.uptime == 0) {
71 logger.info("Profile paramater {} must not be 0", UPTIME_PARAM);
75 if (configuration.downtime == 0) {
76 configuration.downtime = configuration.uptime;
79 if (configuration.precision == 0) {
80 configuration.precision = DEFAULT_PRECISION;
83 this.isValidConfiguration = true;
85 logger.debug("Profile configured with '{}'='{}' ms, '{}'={} ms, '{}'={}", UPTIME_PARAM, configuration.uptime,
86 DOWNTIME_PARAM, configuration.downtime, PRECISION_PARAM, configuration.precision);
90 public ProfileTypeUID getProfileTypeUID() {
91 return PROFILE_TYPE_UID;
95 public void onCommandFromItem(Command command) {
96 logger.debug("onCommandFromItem: {}", command);
98 // pass through command if profile has not been configured properly
99 if (!isValidConfiguration) {
100 callback.handleCommand(command);
104 if (command instanceof UpDownType) {
105 if (command == UpDownType.UP) {
107 } else if (command == UpDownType.DOWN) {
110 } else if (command instanceof StopMoveType) {
113 moveTo(((PercentType) command).intValue());
117 private boolean isMoving() {
118 return (!movingSince.equals(Instant.MIN));
121 private void moveTo(int targetPos) {
122 boolean alreadyMoving = false;
124 if (targetPos < 0 || targetPos > 100) {
125 logger.debug("moveTo() position is invalid: {}", targetPos);
129 int curPos = currentPosition();
130 int posOffset = targetPos - curPos;
134 if (targetPos == position && !isMoving()) {
135 logger.debug("moveTo() position already current: {}", targetPos);
136 if (targetPos == 0) { // always send command if either 0 or 100 in case it is not already in that position
137 callback.handleCommand(UpDownType.UP);
138 } else if (targetPos == 100) {
139 callback.handleCommand(UpDownType.DOWN);
142 } else if (targetPos == 0 || targetPos == 100) {
143 logger.debug("moveTo() bounding position");
144 newCmd = targetPos == 0 ? UpDownType.UP : UpDownType.DOWN;
145 } else if (Math.abs(posOffset) < configuration.precision) {
146 callback.sendUpdate(new PercentType(position)); // update position because autoupdate will assume the
148 logger.info("moveTo() is less than the precision setting of {}", configuration.precision);
151 newCmd = posOffset > 0 ? UpDownType.DOWN : UpDownType.UP;
154 logger.debug("moveTo() targetPosition: {} from currentPosition: {}", targetPos, curPos);
156 long time = (long) ((Math.abs(posOffset) / 100d)
157 * (posOffset > 0 ? (double) configuration.downtime * 1000 : (double) configuration.uptime * 1000));
158 logger.debug("moveTo() computed movement offset: {} / {} / {} ms", posOffset, newCmd, time);
161 position = curPos; // Update "starting" position if already in motion since the last move did not finish
163 if (direction == newCmd) {
164 alreadyMoving = true;
168 this.targetPosition = targetPos;
169 this.direction = newCmd;
170 this.movingSince = Instant.now();
172 if (stopTimer != null) {
173 Objects.requireNonNull(stopTimer).cancel(true);
175 this.stopTimer = scheduler.schedule(stopTimeoutTask, time, TimeUnit.MILLISECONDS);
177 if (updateTimer != null) {
178 Objects.requireNonNull(updateTimer).cancel(true);
180 this.updateTimer = scheduler.scheduleWithFixedDelay(updateTimeoutTask, 0, POSITION_UPDATE_PERIOD_MILLISECONDS,
181 TimeUnit.MILLISECONDS);
183 if (!alreadyMoving) {
184 logger.debug("moveTo() sending command for movement: {}, timer set in {} ms", direction, time);
185 callback.handleCommand(direction);
187 logger.debug("moveTo() updating timing but already moving in right directio: {}, timer set in {} ms",
192 private void stop() {
193 callback.handleCommand(StopMoveType.STOP);
195 this.position = currentPosition();
196 this.movingSince = Instant.MIN;
198 if (stopTimer != null) {
199 Objects.requireNonNull(stopTimer).cancel(true);
200 this.stopTimer = null;
202 if (updateTimer != null) {
203 Objects.requireNonNull(updateTimer).cancel(true);
204 this.updateTimer = null;
207 callback.sendUpdate(new PercentType(position));
210 private int currentPosition() {
212 logger.trace("currentPosition() while moving");
214 // movingSince is always set if moving
215 long millis = movingSince.until(Instant.now(), ChronoUnit.MILLIS);
218 if (direction == UpDownType.UP) {
219 delta = -(millis / (configuration.uptime * 1000)) * 100d;
221 delta = (millis / (configuration.downtime * 1000)) * 100d;
224 return (int) Math.max(0, Math.min(100, Math.round(position + delta)));
230 // Runnable task to time duration of the move to make
231 private Runnable stopTimeoutTask = new Runnable() {
234 if (targetPosition == 0 || targetPosition == 100) {
235 // Don't send stop command to re-sync position using the motor end stop
236 logger.debug("arrived at end position, not stopping for calibration");
238 callback.handleCommand(StopMoveType.STOP);
239 logger.debug("arrived at position, sending STOP command");
242 logger.trace("stopTimeoutTask() position: {}", targetPosition);
244 if (updateTimer != null) {
245 Objects.requireNonNull(updateTimer).cancel(true);
249 movingSince = Instant.MIN;
250 position = targetPosition;
252 callback.sendUpdate(new PercentType(position));
256 // Runnable task to update the item on position while the roller shutter is moving
257 private Runnable updateTimeoutTask = new Runnable() {
261 int pos = currentPosition();
262 if (pos < 0 || pos > 100) {
265 callback.sendUpdate(new PercentType(pos));
266 logger.trace("updateTimeoutTask(): {}", pos);
272 public void onStateUpdateFromItem(State state) {
276 public void onCommandFromHandler(Command command) {
280 public void onStateUpdateFromHandler(State state) {