]> git.basschouten.com Git - openhab-addons.git/blob
192a544b345095e16168eb99a94913125b46f83f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.transform.rollershutterposition.internal;
14
15 import static org.openhab.transform.rollershutterposition.internal.RollerShutterPositionConstants.*;
16
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;
23
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;
39
40 /**
41  * Profile to implement the RollerShutterPosition ItemChannelLink
42  *
43  * @author Jeff James - Initial contribution
44  *
45  *         Core logic in this module has been heavily adapted from Tarag Gautier js script implementation
46  *         VASRollershutter.js
47  */
48 @NonNullByDefault
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);
52
53     private final ProfileCallback callback;
54     RollerShutterPositionConfig configuration;
55
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;
61
62     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(PROFILE_THREADPOOL_NAME);
63     protected @Nullable ScheduledFuture<?> stopTimer = null;
64     protected @Nullable ScheduledFuture<?> updateTimer = null;
65
66     public RollerShutterPositionProfile(final ProfileCallback callback, final ProfileContext context) {
67         this.callback = callback;
68         this.configuration = context.getConfiguration().as(RollerShutterPositionConfig.class);
69
70         if (configuration.uptime == 0) {
71             logger.info("Profile paramater {} must not be 0", UPTIME_PARAM);
72             return;
73         }
74
75         if (configuration.downtime == 0) {
76             configuration.downtime = configuration.uptime;
77         }
78
79         if (configuration.precision == 0) {
80             configuration.precision = DEFAULT_PRECISION;
81         }
82
83         this.isValidConfiguration = true;
84
85         logger.debug("Profile configured with '{}'='{}' ms, '{}'={} ms, '{}'={}", UPTIME_PARAM, configuration.uptime,
86                 DOWNTIME_PARAM, configuration.downtime, PRECISION_PARAM, configuration.precision);
87     }
88
89     @Override
90     public ProfileTypeUID getProfileTypeUID() {
91         return PROFILE_TYPE_UID;
92     }
93
94     @Override
95     public void onCommandFromItem(Command command) {
96         logger.debug("onCommandFromItem: {}", command);
97
98         // pass through command if profile has not been configured properly
99         if (!isValidConfiguration) {
100             callback.handleCommand(command);
101             return;
102         }
103
104         if (command instanceof UpDownType) {
105             if (command == UpDownType.UP) {
106                 moveTo(0);
107             } else if (command == UpDownType.DOWN) {
108                 moveTo(100);
109             }
110         } else if (command instanceof StopMoveType) {
111             stop();
112         } else {
113             moveTo(((PercentType) command).intValue());
114         }
115     }
116
117     private boolean isMoving() {
118         return (!movingSince.equals(Instant.MIN));
119     }
120
121     private void moveTo(int targetPos) {
122         boolean alreadyMoving = false;
123
124         if (targetPos < 0 || targetPos > 100) {
125             logger.debug("moveTo() position is invalid: {}", targetPos);
126             return;
127         }
128
129         int curPos = currentPosition();
130         int posOffset = targetPos - curPos;
131
132         UpDownType newCmd;
133
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);
140             }
141             return;
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
147                                                             // movement happened
148             logger.info("moveTo() is less than the precision setting of {}", configuration.precision);
149             return;
150         } else {
151             newCmd = posOffset > 0 ? UpDownType.DOWN : UpDownType.UP;
152         }
153
154         logger.debug("moveTo() targetPosition: {} from currentPosition: {}", targetPos, curPos);
155
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);
159
160         if (isMoving()) {
161             position = curPos; // Update "starting" position if already in motion since the last move did not finish
162
163             if (direction == newCmd) {
164                 alreadyMoving = true;
165             }
166         }
167
168         this.targetPosition = targetPos;
169         this.direction = newCmd;
170         this.movingSince = Instant.now();
171
172         if (stopTimer != null) {
173             Objects.requireNonNull(stopTimer).cancel(true);
174         }
175         this.stopTimer = scheduler.schedule(stopTimeoutTask, time, TimeUnit.MILLISECONDS);
176
177         if (updateTimer != null) {
178             Objects.requireNonNull(updateTimer).cancel(true);
179         }
180         this.updateTimer = scheduler.scheduleWithFixedDelay(updateTimeoutTask, 0, POSITION_UPDATE_PERIOD_MILLISECONDS,
181                 TimeUnit.MILLISECONDS);
182
183         if (!alreadyMoving) {
184             logger.debug("moveTo() sending command for movement: {}, timer set in {} ms", direction, time);
185             callback.handleCommand(direction);
186         } else {
187             logger.debug("moveTo() updating timing but already moving in right directio: {}, timer set in {} ms",
188                     direction, time);
189         }
190     }
191
192     private void stop() {
193         callback.handleCommand(StopMoveType.STOP);
194
195         this.position = currentPosition();
196         this.movingSince = Instant.MIN;
197
198         if (stopTimer != null) {
199             Objects.requireNonNull(stopTimer).cancel(true);
200             this.stopTimer = null;
201         }
202         if (updateTimer != null) {
203             Objects.requireNonNull(updateTimer).cancel(true);
204             this.updateTimer = null;
205         }
206
207         callback.sendUpdate(new PercentType(position));
208     }
209
210     private int currentPosition() {
211         if (isMoving()) {
212             logger.trace("currentPosition() while moving");
213
214             // movingSince is always set if moving
215             long millis = movingSince.until(Instant.now(), ChronoUnit.MILLIS);
216             double delta = 0;
217
218             if (direction == UpDownType.UP) {
219                 delta = -(millis / (configuration.uptime * 1000)) * 100d;
220             } else {
221                 delta = (millis / (configuration.downtime * 1000)) * 100d;
222             }
223
224             return (int) Math.max(0, Math.min(100, Math.round(position + delta)));
225         } else {
226             return position;
227         }
228     }
229
230     // Runnable task to time duration of the move to make
231     private Runnable stopTimeoutTask = new Runnable() {
232         @Override
233         public void run() {
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");
237             } else {
238                 callback.handleCommand(StopMoveType.STOP);
239                 logger.debug("arrived at position, sending STOP command");
240             }
241
242             logger.trace("stopTimeoutTask() position: {}", targetPosition);
243
244             if (updateTimer != null) {
245                 Objects.requireNonNull(updateTimer).cancel(true);
246                 updateTimer = null;
247             }
248
249             movingSince = Instant.MIN;
250             position = targetPosition;
251             targetPosition = -1;
252             callback.sendUpdate(new PercentType(position));
253         }
254     };
255
256     // Runnable task to update the item on position while the roller shutter is moving
257     private Runnable updateTimeoutTask = new Runnable() {
258         @Override
259         public void run() {
260             if (isMoving()) {
261                 int pos = currentPosition();
262                 if (pos < 0 || pos > 100) {
263                     return;
264                 }
265                 callback.sendUpdate(new PercentType(pos));
266                 logger.trace("updateTimeoutTask(): {}", pos);
267             }
268         }
269     };
270
271     @Override
272     public void onStateUpdateFromItem(State state) {
273     }
274
275     @Override
276     public void onCommandFromHandler(Command command) {
277     }
278
279     @Override
280     public void onStateUpdateFromHandler(State state) {
281     }
282 }