2 * Copyright (c) 2010-2022 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 commands/messages for an Automation OpenWebNet
51 * device. It extends the abstract {@link OpenWebNetThingHandler}.
53 * @author Massimo Valla - Initial contribution
56 public class OpenWebNetAutomationHandler extends OpenWebNetThingHandler {
58 private final Logger logger = LoggerFactory.getLogger(OpenWebNetAutomationHandler.class);
60 private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("ss.SSS");
62 private static long lastAllDevicesRefreshTS = -1; // timestamp when the last request for all device refresh was sent
63 protected static final int ALL_DEVICES_REFRESH_INTERVAL_MSEC = 60000; // interval in msec before sending another all
64 // devices refresh request
66 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.AUTOMATION_SUPPORTED_THING_TYPES;
69 public static final int MOVING_STATE_STOPPED = 0;
70 public static final int MOVING_STATE_MOVING_UP = 1;
71 public static final int MOVING_STATE_MOVING_DOWN = 2;
72 public static final int MOVING_STATE_UNKNOWN = -1;
75 public static final int CALIBRATION_INACTIVE = -1;
76 public static final int CALIBRATION_ACTIVATED = 0;
77 public static final int CALIBRATION_GOING_UP = 1;
78 public static final int CALIBRATION_GOING_DOWN = 2;
81 public static final int POSITION_MAX_STEPS = 100;
82 public static final int POSITION_DOWN = 100;
83 public static final int POSITION_UP = 0;
84 public static final int POSITION_UNKNOWN = -1;
86 public static final int SHUTTER_RUN_UNDEFINED = -1;
87 private int shutterRun = SHUTTER_RUN_UNDEFINED;
88 private static final String AUTO_CALIBRATION = "AUTO";
90 private long startedMovingAtTS = -1; // timestamp when device started moving UP/DOWN
91 private int movingState = MOVING_STATE_UNKNOWN;
92 private int positionEstimation = POSITION_UNKNOWN;
93 private @Nullable ScheduledFuture<?> moveSchedule;
94 private int positionRequested = POSITION_UNKNOWN;
95 private int calibrating = CALIBRATION_INACTIVE;
96 private static final int MIN_STEP_TIME_MSEC = 50;
97 private @Nullable Command commandRequestedWhileMoving = null;
99 public OpenWebNetAutomationHandler(Thing thing) {
104 public void initialize() {
106 Object shutterRunConfig = getConfig().get(OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN);
108 if (shutterRunConfig == null) {
109 shutterRunConfig = AUTO_CALIBRATION;
110 logger.debug("shutterRun null --> default to AUTO");
111 } else if (shutterRunConfig instanceof String) {
112 if (AUTO_CALIBRATION.equalsIgnoreCase(((String) shutterRunConfig))) {
113 logger.debug("shutterRun set to AUTO via configuration");
114 shutterRun = SHUTTER_RUN_UNDEFINED; // reset shutterRun
115 } else { // try to parse int>=1000
116 int shutterRunInt = Integer.parseInt((String) shutterRunConfig);
117 if (shutterRunInt < 1000) {
118 throw new NumberFormatException();
120 shutterRun = shutterRunInt;
121 logger.debug("shutterRun set to {} via configuration", shutterRun);
124 throw new NumberFormatException();
126 } catch (NumberFormatException e) {
127 logger.debug("Wrong configuration: {} setting must be {} or an integer >= 1000",
128 OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN, AUTO_CALIBRATION);
129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
130 "@text/offline.wrong-configuration");
131 shutterRun = SHUTTER_RUN_UNDEFINED;
133 updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
134 positionEstimation = POSITION_UNKNOWN;
138 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
139 return new WhereLightAutom(wStr);
143 protected void requestChannelState(ChannelUID channel) {
144 logger.debug("requestChannelState() thingUID={} channel={}", thing.getUID(), channel.getId());
145 Where w = deviceWhere;
148 send(Automation.requestStatus(w.value()));
149 } catch (OWNException e) {
150 logger.debug("Exception while requesting channel {} state: {}", channel, e.getMessage(), e);
153 logger.warn("Could not requestChannelState(): deviceWhere is null");
158 protected void refreshDevice(boolean refreshAll) {
159 OpenWebNetBridgeHandler brH = bridgeHandler;
161 if (brH.isBusGateway() && refreshAll) {
162 long now = System.currentTimeMillis();
163 if (now - lastAllDevicesRefreshTS > ALL_DEVICES_REFRESH_INTERVAL_MSEC) {
165 send(Automation.requestStatus(WhereLightAutom.GENERAL.value()));
166 lastAllDevicesRefreshTS = now;
167 } catch (OWNException e) {
168 logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
171 logger.debug("Refresh all devices just sent...");
174 requestChannelState(new ChannelUID("any")); // channel here does not make any difference
180 protected void handleChannelCommand(ChannelUID channel, Command command) {
181 switch (channel.getId()) {
182 case CHANNEL_SHUTTER:
183 handleShutterCommand(command);
186 logger.info("Unsupported channel UID {}", channel);
192 * Handles Automation Roller shutter command (UP/DOWN, STOP/MOVE, PERCENT xx%)
194 private void handleShutterCommand(Command command) {
195 Where w = deviceWhere;
197 calibrating = CALIBRATION_INACTIVE; // cancel calibration if we receive a command
198 commandRequestedWhileMoving = null;
200 if (StopMoveType.STOP.equals(command)) {
201 send(Automation.requestStop(w.value()));
202 } else if (command instanceof UpDownType || command instanceof PercentType) {
203 if (movingState == MOVING_STATE_MOVING_UP || movingState == MOVING_STATE_MOVING_DOWN) { // already
205 logger.debug("# {} # already moving, STOP then defer command", deviceWhere);
206 commandRequestedWhileMoving = command;
207 sendHighPriority(Automation.requestStop(w.value()));
210 if (command instanceof UpDownType) {
211 if (UpDownType.UP.equals(command)) {
212 send(Automation.requestMoveUp(w.value()));
214 send(Automation.requestMoveDown(w.value()));
216 } else if (command instanceof PercentType) {
217 handlePercentCommand((PercentType) command, w.value());
221 logger.debug("Unsupported command {} for thing {}", command, thing.getUID());
223 } catch (OWNException e) {
224 logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
230 * Handles Automation PERCENT xx% command
232 private void handlePercentCommand(PercentType command, String w) {
233 int percent = command.intValue();
234 if (percent == positionEstimation) {
235 logger.debug("# {} # handlePercentCommand() Command {}% == positionEstimation -> nothing to do", w,
240 if (percent == POSITION_DOWN) { // GO TO 100%
241 send(Automation.requestMoveDown(w));
242 } else if (percent == POSITION_UP) { // GO TO 0%
243 send(Automation.requestMoveUp(w));
244 } else { // GO TO XX%
245 logger.debug("# {} # {}% requested", deviceWhere, percent);
246 if (shutterRun == SHUTTER_RUN_UNDEFINED) {
247 logger.debug("& {} & CALIBRATION - shutterRun not configured, starting CALIBRATION...",
249 calibrating = CALIBRATION_ACTIVATED;
250 send(Automation.requestMoveUp(w));
251 positionRequested = percent;
252 } else if (shutterRun >= 1000 && positionEstimation != POSITION_UNKNOWN) {
253 // these two must be known to calculate moveTime.
254 // Calculate how much time we have to move and set a deadline to stop after that time
256 .round(((float) Math.abs(percent - positionEstimation) / POSITION_MAX_STEPS * shutterRun));
257 logger.debug("# {} # target moveTime={}", deviceWhere, moveTime);
258 if (moveTime > MIN_STEP_TIME_MSEC) {
259 ScheduledFuture<?> mSch = moveSchedule;
260 if (mSch != null && !mSch.isDone()) {
261 // a moveSchedule was already scheduled and is not done... let's cancel the schedule
263 logger.debug("# {} # new XX% requested, old moveSchedule cancelled", deviceWhere);
265 // send a requestFirmwareVersion message to BUS gateways to wake up the CMD connection before
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 command waits for
278 // ACK and can take some 300ms
279 logger.debug("# {} # Starting schedule...", deviceWhere);
280 moveSchedule = scheduler.schedule(() -> {
281 logger.debug("# {} # moveSchedule expired, sending STOP...", deviceWhere);
283 sendHighPriority(Automation.requestStop(w));
284 } catch (OWNException ex) {
285 logger.debug("Exception while sending request for command {}: {}", command,
286 ex.getMessage(), ex);
288 }, moveTime, TimeUnit.MILLISECONDS);
289 logger.debug("# {} # ...schedule started, now sending highPriority command...", deviceWhere);
290 if (percent < positionEstimation) {
291 sendHighPriority(Automation.requestMoveUp(w));
293 sendHighPriority(Automation.requestMoveDown(w));
295 logger.debug("# {} # ...gateway.sendHighPriority() returned", deviceWhere);
297 logger.debug("# {} # moveTime <= MIN_STEP_TIME_MSEC ---> do nothing", deviceWhere);
301 "Command {} cannot be executed: unknown position or shutterRun configuration params not/wrongly set (thing={})",
302 command, thing.getUID());
305 } catch (OWNException e) {
306 logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
311 protected String ownIdPrefix() {
312 return Who.AUTOMATION.value().toString();
316 protected void handleMessage(BaseOpenMessage msg) {
317 logger.debug("handleMessage({}) for thing: {}", msg, thing.getUID());
318 updateAutomationState((Automation) msg);
319 // REMINDER: update automation state, and only after update thing status using the super method, to avoid delays
320 super.handleMessage(msg);
324 * Updates automation device state based on the Automation message received from OWN network
326 * @param msg the Automation message
328 private void updateAutomationState(Automation msg) {
329 logger.debug("updateAutomationState() - msg={} what={}", msg, msg.getWhat());
331 if (msg.isCommandTranslation()) {
332 logger.debug("msg is command translation, ignoring it");
335 } catch (FrameException fe) {
336 logger.warn("Exception while checking WHERE command translation for frame {}: {}, ignoring it", msg,
340 updateMovingState(MOVING_STATE_MOVING_UP);
341 if (calibrating == CALIBRATION_ACTIVATED) {
342 calibrating = CALIBRATION_GOING_UP;
343 logger.debug("& {} & CALIBRATION - started going ALL UP...", deviceWhere);
345 } else if (msg.isDown()) {
346 updateMovingState(MOVING_STATE_MOVING_DOWN);
347 if (calibrating == CALIBRATION_ACTIVATED) {
348 calibrating = CALIBRATION_GOING_DOWN;
349 logger.debug("& {} & CALIBRATION - started going ALL DOWN...", deviceWhere);
351 } else if (msg.isStop()) {
352 long stoppedAt = System.currentTimeMillis();
353 if (calibrating == CALIBRATION_GOING_DOWN && shutterRun == SHUTTER_RUN_UNDEFINED) {
354 shutterRun = (int) (stoppedAt - startedMovingAtTS);
355 logger.debug("& {} & CALIBRATION - reached DOWN ---> shutterRun={}", deviceWhere, shutterRun);
356 updateMovingState(MOVING_STATE_STOPPED);
357 logger.debug("& {} & CALIBRATION - COMPLETED, now going to {}%", deviceWhere, positionRequested);
358 handleShutterCommand(new PercentType(positionRequested));
359 Configuration configuration = editConfiguration();
360 configuration.put(OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN, Integer.toString(shutterRun));
361 updateConfiguration(configuration);
362 logger.debug("& {} & CALIBRATION - configuration updated: shutterRun = {}ms", deviceWhere, shutterRun);
363 } else if (calibrating == CALIBRATION_GOING_UP) {
364 updateMovingState(MOVING_STATE_STOPPED);
365 logger.debug("& {} & CALIBRATION - reached UP, now sending DOWN command...", deviceWhere);
366 calibrating = CALIBRATION_ACTIVATED;
367 Where dw = deviceWhere;
369 String w = dw.value();
371 send(Automation.requestMoveDown(w));
372 } catch (OWNException e) {
373 logger.debug("Exception while sending DOWN command during calibration: {}", e.getMessage(), e);
374 calibrating = CALIBRATION_INACTIVE;
378 updateMovingState(MOVING_STATE_STOPPED);
379 // do deferred command, if present
380 Command cmd = commandRequestedWhileMoving;
382 handleShutterCommand(cmd);
386 logger.debug("Frame {} not supported for thing {}, ignoring it.", msg, thing.getUID());
391 * Updates movingState to newState
393 private void updateMovingState(int newState) {
394 if (movingState == MOVING_STATE_STOPPED) {
395 if (newState != MOVING_STATE_STOPPED) { // moving after stop
396 startedMovingAtTS = System.currentTimeMillis();
397 synchronized (DATE_FORMATTER) {
398 logger.debug("# {} # MOVING {} - startedMovingAt={} - {}", deviceWhere, newState, startedMovingAtTS,
399 DATE_FORMATTER.format(new Date(startedMovingAtTS)));
402 } else { // we were moving
404 if (newState != MOVING_STATE_STOPPED) { // moving after moving, take new timestamp
405 startedMovingAtTS = System.currentTimeMillis();
406 synchronized (DATE_FORMATTER) {
407 logger.debug("# {} # MOVING {} - startedMovingAt={} - {}", deviceWhere, newState, startedMovingAtTS,
408 DATE_FORMATTER.format(new Date(startedMovingAtTS)));
411 // cancel the schedule
412 ScheduledFuture<?> mSc = moveSchedule;
413 if (mSc != null && !mSc.isDone()) {
417 movingState = newState;
418 logger.debug("# {} # movingState={} positionEstimation={} - calibrating={} shutterRun={}", deviceWhere,
419 movingState, positionEstimation, calibrating, shutterRun);
423 * Updates positionEstimation and then channel state based on movedTime and current movingState
425 private void updatePosition() {
426 int newPos = POSITION_UNKNOWN;
427 if (shutterRun > 0 && startedMovingAtTS != -1) {// we have shutterRun and startedMovingAtTS defined, let's
428 // calculate new positionEstimation
429 long movedTime = System.currentTimeMillis() - startedMovingAtTS;
430 logger.debug("# {} # current positionEstimation={} movedTime={}", deviceWhere, positionEstimation,
432 int movedSteps = Math.round((float) movedTime / shutterRun * POSITION_MAX_STEPS);
433 logger.debug("# {} # movedSteps: {} {}", deviceWhere, movedSteps,
434 (movingState == MOVING_STATE_MOVING_DOWN) ? "DOWN(+)" : "UP(-)");
435 if (positionEstimation == POSITION_UNKNOWN && movedSteps >= POSITION_MAX_STEPS) { // we did a full run
436 newPos = (movingState == MOVING_STATE_MOVING_DOWN) ? POSITION_DOWN : POSITION_UP;
437 } else if (positionEstimation != POSITION_UNKNOWN) {
438 newPos = positionEstimation
439 + ((movingState == MOVING_STATE_MOVING_DOWN) ? movedSteps : -1 * movedSteps);
440 logger.debug("# {} # {} {} {} = {}", deviceWhere, positionEstimation,
441 (movingState == MOVING_STATE_MOVING_DOWN) ? "+" : "-", movedSteps, newPos);
442 if (newPos > POSITION_DOWN) {
443 newPos = POSITION_DOWN;
444 } else if (newPos < POSITION_UP) {
445 newPos = POSITION_UP;
449 if (newPos != POSITION_UNKNOWN) {
450 if (newPos != positionEstimation) {
451 updateState(CHANNEL_SHUTTER, new PercentType(newPos));
454 updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
456 positionEstimation = newPos;
460 public void dispose() {
461 ScheduledFuture<?> mSc = moveSchedule;