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.openwebnet.internal.handler;
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.CHANNEL_SHUTTER;
17 import java.text.SimpleDateFormat;
18 import java.util.Date;
20 import java.util.concurrent.ScheduledFuture;
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.openwebnet.internal.OpenWebNetBindingConstants;
26 import org.openhab.core.config.core.Configuration;
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.ChannelUID;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
34 import org.openhab.core.thing.ThingTypeUID;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.UnDefType;
37 import org.openwebnet4j.OpenGateway;
38 import org.openwebnet4j.communication.OWNException;
39 import org.openwebnet4j.message.Automation;
40 import org.openwebnet4j.message.BaseOpenMessage;
41 import org.openwebnet4j.message.FrameException;
42 import org.openwebnet4j.message.GatewayMgmt;
43 import org.openwebnet4j.message.Where;
44 import org.openwebnet4j.message.WhereLightAutom;
45 import org.openwebnet4j.message.Who;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link OpenWebNetAutomationHandler} is responsible for handling
51 * commands/messages for an Automation OpenWebNet
52 * device. It extends the abstract {@link OpenWebNetThingHandler}.
54 * @author Massimo Valla - Initial contribution
57 public class OpenWebNetAutomationHandler extends OpenWebNetThingHandler {
59 private final Logger logger = LoggerFactory.getLogger(OpenWebNetAutomationHandler.class);
61 private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("ss.SSS");
63 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.AUTOMATION_SUPPORTED_THING_TYPES;
65 private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
68 public static final int MOVING_STATE_STOPPED = 0;
69 public static final int MOVING_STATE_MOVING_UP = 1;
70 public static final int MOVING_STATE_MOVING_DOWN = 2;
71 public static final int MOVING_STATE_UNKNOWN = -1;
74 public static final int CALIBRATION_INACTIVE = -1;
75 public static final int CALIBRATION_ACTIVATED = 0;
76 public static final int CALIBRATION_GOING_UP = 1;
77 public static final int CALIBRATION_GOING_DOWN = 2;
80 public static final int POSITION_MAX_STEPS = 100;
81 public static final int POSITION_DOWN = 100;
82 public static final int POSITION_UP = 0;
83 public static final int POSITION_UNKNOWN = -1;
85 public static final int SHUTTER_RUN_UNDEFINED = -1;
86 private int shutterRun = SHUTTER_RUN_UNDEFINED;
87 private static final String AUTO_CALIBRATION = "AUTO";
89 private long startedMovingAtTS = -1; // timestamp when device started moving UP/DOWN
90 private int movingState = MOVING_STATE_UNKNOWN;
91 private int positionEstimation = POSITION_UNKNOWN;
92 private @Nullable ScheduledFuture<?> moveSchedule;
93 private int positionRequested = POSITION_UNKNOWN;
94 private int calibrating = CALIBRATION_INACTIVE;
95 private static final int MIN_STEP_TIME_MSEC = 50;
96 private @Nullable Command commandRequestedWhileMoving = null;
98 public OpenWebNetAutomationHandler(Thing thing) {
103 public void initialize() {
105 Object shutterRunConfig = getConfig().get(OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN);
107 if (shutterRunConfig == null) {
108 shutterRunConfig = AUTO_CALIBRATION;
109 logger.debug("shutterRun null --> default to AUTO");
110 } else if (shutterRunConfig instanceof String) {
111 if (AUTO_CALIBRATION.equalsIgnoreCase(((String) shutterRunConfig))) {
112 logger.debug("shutterRun set to AUTO via configuration");
113 shutterRun = SHUTTER_RUN_UNDEFINED; // reset shutterRun
114 } else { // try to parse int>=1000
115 int shutterRunInt = Integer.parseInt((String) shutterRunConfig);
116 if (shutterRunInt < 1000) {
117 throw new NumberFormatException();
119 shutterRun = shutterRunInt;
120 logger.debug("shutterRun set to {} via configuration", shutterRun);
123 throw new NumberFormatException();
125 } catch (NumberFormatException e) {
126 logger.debug("Wrong configuration: {} setting must be {} or an integer >= 1000",
127 OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN, AUTO_CALIBRATION);
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
129 "@text/offline.wrong-configuration");
130 shutterRun = SHUTTER_RUN_UNDEFINED;
132 updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
133 positionEstimation = POSITION_UNKNOWN;
137 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
138 return new WhereLightAutom(wStr);
142 protected void requestChannelState(ChannelUID channel) {
143 super.requestChannelState(channel);
144 Where w = deviceWhere;
147 send(Automation.requestStatus(w.value()));
148 } catch (OWNException e) {
149 logger.debug("Exception while requesting state for channel {}: {} ", channel, e.getMessage());
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
156 protected long getRefreshAllLastTS() {
157 return lastAllDevicesRefreshTS;
161 protected void refreshDevice(boolean refreshAll) {
163 logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
165 send(Automation.requestStatus(WhereLightAutom.GENERAL.value()));
166 lastAllDevicesRefreshTS = System.currentTimeMillis();
167 } catch (OWNException e) {
168 logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
171 logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
172 requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_SHUTTER));
177 protected void handleChannelCommand(ChannelUID channel, Command command) {
178 switch (channel.getId()) {
179 case CHANNEL_SHUTTER:
180 handleShutterCommand(command);
183 logger.info("Unsupported channel UID {}", channel);
189 * Handles Automation Roller shutter command (UP/DOWN, STOP/MOVE, PERCENT xx%)
191 private void handleShutterCommand(Command command) {
192 Where w = deviceWhere;
194 calibrating = CALIBRATION_INACTIVE; // cancel calibration if we receive a command
195 commandRequestedWhileMoving = null;
197 if (StopMoveType.STOP.equals(command)) {
198 send(Automation.requestStop(w.value()));
199 } else if (command instanceof UpDownType || command instanceof PercentType) {
200 if (movingState == MOVING_STATE_MOVING_UP || movingState == MOVING_STATE_MOVING_DOWN) { // already
202 logger.debug("# {} # already moving, STOP then defer command", deviceWhere);
203 commandRequestedWhileMoving = command;
204 sendHighPriority(Automation.requestStop(w.value()));
207 if (command instanceof UpDownType) {
208 if (UpDownType.UP.equals(command)) {
209 send(Automation.requestMoveUp(w.value()));
211 send(Automation.requestMoveDown(w.value()));
213 } else if (command instanceof PercentType) {
214 handlePercentCommand((PercentType) command, w.value());
218 logger.debug("Unsupported command {} for thing {}", command, thing.getUID());
220 } catch (OWNException e) {
221 logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
227 * Handles Automation PERCENT xx% command
229 private void handlePercentCommand(PercentType command, String w) {
230 int percent = command.intValue();
231 if (percent == positionEstimation) {
232 logger.debug("# {} # handlePercentCommand() Command {}% == positionEstimation -> nothing to do", w,
237 if (percent == POSITION_DOWN) { // GO TO 100%
238 send(Automation.requestMoveDown(w));
239 } else if (percent == POSITION_UP) { // GO TO 0%
240 send(Automation.requestMoveUp(w));
241 } else { // GO TO XX%
242 logger.debug("# {} # {}% requested", deviceWhere, percent);
243 if (shutterRun == SHUTTER_RUN_UNDEFINED) {
244 logger.debug("& {} & CALIBRATION - shutterRun not configured, starting CALIBRATION...",
246 calibrating = CALIBRATION_ACTIVATED;
247 send(Automation.requestMoveUp(w));
248 positionRequested = percent;
249 } else if (shutterRun >= 1000 && positionEstimation != POSITION_UNKNOWN) {
250 // these two must be known to calculate moveTime.
251 // Calculate how much time we have to move and set a deadline to stop after that
254 .round(((float) Math.abs(percent - positionEstimation) / POSITION_MAX_STEPS * shutterRun));
255 logger.debug("# {} # target moveTime={}", deviceWhere, moveTime);
256 if (moveTime > MIN_STEP_TIME_MSEC) {
257 ScheduledFuture<?> mSch = moveSchedule;
258 if (mSch != null && !mSch.isDone()) {
259 // a moveSchedule was already scheduled and is not done... let's cancel the
262 logger.debug("# {} # new XX% requested, old moveSchedule cancelled", deviceWhere);
264 // send a requestFirmwareVersion message to BUS gateways to wake up the CMD
266 // sending further cmds
267 OpenWebNetBridgeHandler h = bridgeHandler;
268 if (h != null && h.isBusGateway()) {
269 OpenGateway gw = h.gateway;
271 if (!gw.isCmdConnectionReady()) {
272 logger.debug("# {} # waking-up CMD connection...", deviceWhere);
273 send(GatewayMgmt.requestFirmwareVersion());
277 // REMINDER: start the schedule BEFORE sending the command, because the synch
279 // ACK and can take some 300ms
280 logger.debug("# {} # Starting schedule...", deviceWhere);
281 moveSchedule = scheduler.schedule(() -> {
282 logger.debug("# {} # moveSchedule expired, sending STOP...", deviceWhere);
284 sendHighPriority(Automation.requestStop(w));
285 } catch (OWNException ex) {
286 logger.debug("Exception while sending request for command {}: {}", command,
287 ex.getMessage(), ex);
289 }, moveTime, TimeUnit.MILLISECONDS);
290 logger.debug("# {} # ...schedule started, now sending highPriority command...", deviceWhere);
291 if (percent < positionEstimation) {
292 sendHighPriority(Automation.requestMoveUp(w));
294 sendHighPriority(Automation.requestMoveDown(w));
296 logger.debug("# {} # ...gateway.sendHighPriority() returned", deviceWhere);
298 logger.debug("# {} # moveTime <= MIN_STEP_TIME_MSEC ---> do nothing", deviceWhere);
302 "Command {} cannot be executed: UNDEF position or shutterRun configuration parameter not/wrongly set (thing={})",
303 command, thing.getUID());
306 } catch (OWNException e) {
307 logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
312 protected String ownIdPrefix() {
313 return Who.AUTOMATION.value().toString();
317 protected void handleMessage(BaseOpenMessage msg) {
318 logger.debug("handleMessage({}) for thing: {}", msg, thing.getUID());
319 updateAutomationState((Automation) msg);
320 // REMINDER: update automation state, and only after update thing status using
321 // the super method, to avoid delays
322 super.handleMessage(msg);
326 * Updates automation device state based on the Automation message received from
329 * @param msg the Automation message
331 private void updateAutomationState(Automation msg) {
332 logger.debug("updateAutomationState() - msg={} what={}", msg, msg.getWhat());
334 if (msg.isCommandTranslation()) {
335 logger.debug("msg is command translation, ignoring it");
338 } catch (FrameException fe) {
339 logger.warn("Exception while checking WHERE command translation for frame {}: {}, ignoring it", msg,
343 updateMovingState(MOVING_STATE_MOVING_UP);
344 if (calibrating == CALIBRATION_ACTIVATED) {
345 calibrating = CALIBRATION_GOING_UP;
346 logger.debug("& {} & CALIBRATION - started going ALL UP...", deviceWhere);
348 } else if (msg.isDown()) {
349 updateMovingState(MOVING_STATE_MOVING_DOWN);
350 if (calibrating == CALIBRATION_ACTIVATED) {
351 calibrating = CALIBRATION_GOING_DOWN;
352 logger.debug("& {} & CALIBRATION - started going ALL DOWN...", deviceWhere);
354 } else if (msg.isStop()) {
355 long measuredRuntime = System.currentTimeMillis() - startedMovingAtTS;
356 if (calibrating == CALIBRATION_GOING_DOWN && shutterRun == SHUTTER_RUN_UNDEFINED) {
357 // since there are transmission delays we set shutterRun slightly less (-500ms
358 // and -2%) than measuredRuntime
359 shutterRun = (int) ((measuredRuntime - 500) * 0.98);
360 logger.debug("& {} & CALIBRATION - reached DOWN : measuredRuntime={}", deviceWhere, measuredRuntime);
361 updateMovingState(MOVING_STATE_STOPPED);
362 logger.debug("& {} & CALIBRATION - COMPLETED, now going to {}%", deviceWhere, positionRequested);
363 handleShutterCommand(new PercentType(positionRequested));
364 Configuration configuration = editConfiguration();
365 configuration.put(OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN, Integer.toString(shutterRun));
366 updateConfiguration(configuration);
367 logger.debug("& {} & CALIBRATION - configuration updated: shutterRun = {}ms", deviceWhere, shutterRun);
368 } else if (calibrating == CALIBRATION_GOING_UP) {
369 updateMovingState(MOVING_STATE_STOPPED);
370 logger.debug("& {} & CALIBRATION - reached UP, now sending DOWN command...", deviceWhere);
371 calibrating = CALIBRATION_ACTIVATED;
372 Where dw = deviceWhere;
374 String w = dw.value();
376 send(Automation.requestMoveDown(w));
377 } catch (OWNException e) {
378 logger.debug("Exception while sending DOWN command during calibration: {}", e.getMessage(), e);
379 calibrating = CALIBRATION_INACTIVE;
383 updateMovingState(MOVING_STATE_STOPPED);
384 // do deferred command, if present
385 Command cmd = commandRequestedWhileMoving;
387 handleShutterCommand(cmd);
391 logger.debug("Frame {} not supported for thing {}, ignoring it.", msg, thing.getUID());
396 * Updates movingState to newState
398 private void updateMovingState(int newState) {
399 if (movingState == MOVING_STATE_STOPPED) {
400 if (newState != MOVING_STATE_STOPPED) { // moving after stop
401 startedMovingAtTS = System.currentTimeMillis();
402 synchronized (DATE_FORMATTER) {
403 logger.debug("# {} # MOVING {} - startedMovingAt={} - {}", deviceWhere, newState, startedMovingAtTS,
404 DATE_FORMATTER.format(new Date(startedMovingAtTS)));
407 } else { // we were moving
409 if (newState != MOVING_STATE_STOPPED) { // moving after moving, take new timestamp
410 startedMovingAtTS = System.currentTimeMillis();
411 synchronized (DATE_FORMATTER) {
412 logger.debug("# {} # MOVING {} - startedMovingAt={} - {}", deviceWhere, newState, startedMovingAtTS,
413 DATE_FORMATTER.format(new Date(startedMovingAtTS)));
416 // cancel the schedule
417 ScheduledFuture<?> mSc = moveSchedule;
418 if (mSc != null && !mSc.isDone()) {
422 movingState = newState;
423 logger.debug("# {} # movingState={} positionEstimation={} - calibrating={} shutterRun={}", deviceWhere,
424 movingState, positionEstimation, calibrating, shutterRun);
428 * Updates positionEstimation and then channel state based on movedTime and
429 * current movingState
431 private void updatePosition() {
432 int newPos = POSITION_UNKNOWN;
433 if (shutterRun > 0 && startedMovingAtTS != -1) {// we have shutterRun and startedMovingAtTS defined, let's
434 // calculate new positionEstimation
435 long movedTime = System.currentTimeMillis() - startedMovingAtTS;
436 logger.debug("# {} # current positionEstimation={} movedTime={}", deviceWhere, positionEstimation,
438 int movedSteps = Math.round((float) movedTime / shutterRun * POSITION_MAX_STEPS);
439 logger.debug("# {} # movedSteps: {} {}", deviceWhere, movedSteps,
440 (movingState == MOVING_STATE_MOVING_DOWN) ? "DOWN(+)" : "UP(-)");
441 if (positionEstimation == POSITION_UNKNOWN && movedSteps >= POSITION_MAX_STEPS) { // we did a full run
442 newPos = (movingState == MOVING_STATE_MOVING_DOWN) ? POSITION_DOWN : POSITION_UP;
443 } else if (positionEstimation != POSITION_UNKNOWN) {
444 newPos = positionEstimation
445 + ((movingState == MOVING_STATE_MOVING_DOWN) ? movedSteps : -1 * movedSteps);
446 logger.debug("# {} # {} {} {} = {}", deviceWhere, positionEstimation,
447 (movingState == MOVING_STATE_MOVING_DOWN) ? "+" : "-", movedSteps, newPos);
448 if (newPos > POSITION_DOWN) {
449 newPos = POSITION_DOWN;
450 } else if (newPos < POSITION_UP) {
451 newPos = POSITION_UP;
455 if (newPos != POSITION_UNKNOWN) {
456 if (newPos != positionEstimation) {
457 updateState(CHANNEL_SHUTTER, new PercentType(newPos));
460 updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
462 positionEstimation = newPos;
466 public void dispose() {
467 ScheduledFuture<?> mSc = moveSchedule;