]> git.basschouten.com Git - openhab-addons.git/blob
e5ba0561c9af779916964c0d6dbc4a9f059fc79e
[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.binding.openwebnet.internal.handler;
14
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.CHANNEL_SHUTTER;
16
17 import java.text.SimpleDateFormat;
18 import java.util.Date;
19 import java.util.Set;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
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;
48
49 /**
50  * The {@link OpenWebNetAutomationHandler} is responsible for handling
51  * commands/messages for an Automation OpenWebNet
52  * device. It extends the abstract {@link OpenWebNetThingHandler}.
53  *
54  * @author Massimo Valla - Initial contribution
55  */
56 @NonNullByDefault
57 public class OpenWebNetAutomationHandler extends OpenWebNetThingHandler {
58
59     private final Logger logger = LoggerFactory.getLogger(OpenWebNetAutomationHandler.class);
60
61     private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("ss.SSS");
62
63     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.AUTOMATION_SUPPORTED_THING_TYPES;
64
65     private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
66
67     // moving states
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;
72
73     // calibration states
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;
78
79     // positions
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;
84
85     public static final int SHUTTER_RUN_UNDEFINED = -1;
86     private int shutterRun = SHUTTER_RUN_UNDEFINED;
87     private static final String AUTO_CALIBRATION = "AUTO";
88
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;
97
98     public OpenWebNetAutomationHandler(Thing thing) {
99         super(thing);
100     }
101
102     @Override
103     public void initialize() {
104         super.initialize();
105         Object shutterRunConfig = getConfig().get(OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN);
106         try {
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();
118                     }
119                     shutterRun = shutterRunInt;
120                     logger.debug("shutterRun set to {} via configuration", shutterRun);
121                 }
122             } else {
123                 throw new NumberFormatException();
124             }
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;
131         }
132         updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
133         positionEstimation = POSITION_UNKNOWN;
134     }
135
136     @Override
137     protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
138         return new WhereLightAutom(wStr);
139     }
140
141     @Override
142     protected void requestChannelState(ChannelUID channel) {
143         super.requestChannelState(channel);
144         Where w = deviceWhere;
145         if (w != null) {
146             try {
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());
151             }
152         }
153     }
154
155     @Override
156     protected long getRefreshAllLastTS() {
157         return lastAllDevicesRefreshTS;
158     };
159
160     @Override
161     protected void refreshDevice(boolean refreshAll) {
162         if (refreshAll) {
163             logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
164             try {
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());
169             }
170         } else {
171             logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
172             requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_SHUTTER));
173         }
174     }
175
176     @Override
177     protected void handleChannelCommand(ChannelUID channel, Command command) {
178         switch (channel.getId()) {
179             case CHANNEL_SHUTTER:
180                 handleShutterCommand(command);
181                 break;
182             default: {
183                 logger.info("Unsupported channel UID {}", channel);
184             }
185         }
186     }
187
188     /**
189      * Handles Automation Roller shutter command (UP/DOWN, STOP/MOVE, PERCENT xx%)
190      */
191     private void handleShutterCommand(Command command) {
192         Where w = deviceWhere;
193         if (w != null) {
194             calibrating = CALIBRATION_INACTIVE; // cancel calibration if we receive a command
195             commandRequestedWhileMoving = null;
196             try {
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
201                                                                                                             // moving
202                         logger.debug("# {} # already moving, STOP then defer command", deviceWhere);
203                         commandRequestedWhileMoving = command;
204                         sendHighPriority(Automation.requestStop(w.value()));
205                         return;
206                     } else {
207                         if (command instanceof UpDownType) {
208                             if (UpDownType.UP.equals(command)) {
209                                 send(Automation.requestMoveUp(w.value()));
210                             } else {
211                                 send(Automation.requestMoveDown(w.value()));
212                             }
213                         } else if (command instanceof PercentType) {
214                             handlePercentCommand((PercentType) command, w.value());
215                         }
216                     }
217                 } else {
218                     logger.debug("Unsupported command {} for thing {}", command, thing.getUID());
219                 }
220             } catch (OWNException e) {
221                 logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
222             }
223         }
224     }
225
226     /**
227      * Handles Automation PERCENT xx% command
228      */
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,
233                     percent);
234             return;
235         }
236         try {
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...",
245                             deviceWhere);
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
252                     // time
253                     int moveTime = Math
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
260                             // schedule
261                             mSch.cancel(false);
262                             logger.debug("# {} # new XX% requested, old moveSchedule cancelled", deviceWhere);
263                         }
264                         // send a requestFirmwareVersion message to BUS gateways to wake up the CMD
265                         // connection before
266                         // sending further cmds
267                         OpenWebNetBridgeHandler h = bridgeHandler;
268                         if (h != null && h.isBusGateway()) {
269                             OpenGateway gw = h.gateway;
270                             if (gw != null) {
271                                 if (!gw.isCmdConnectionReady()) {
272                                     logger.debug("# {} # waking-up CMD connection...", deviceWhere);
273                                     send(GatewayMgmt.requestFirmwareVersion());
274                                 }
275                             }
276                         }
277                         // REMINDER: start the schedule BEFORE sending the command, because the synch
278                         // command waits for
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);
283                             try {
284                                 sendHighPriority(Automation.requestStop(w));
285                             } catch (OWNException ex) {
286                                 logger.debug("Exception while sending request for command {}: {}", command,
287                                         ex.getMessage(), ex);
288                             }
289                         }, moveTime, TimeUnit.MILLISECONDS);
290                         logger.debug("# {} # ...schedule started, now sending highPriority command...", deviceWhere);
291                         if (percent < positionEstimation) {
292                             sendHighPriority(Automation.requestMoveUp(w));
293                         } else {
294                             sendHighPriority(Automation.requestMoveDown(w));
295                         }
296                         logger.debug("# {} # ...gateway.sendHighPriority() returned", deviceWhere);
297                     } else {
298                         logger.debug("# {} # moveTime <= MIN_STEP_TIME_MSEC ---> do nothing", deviceWhere);
299                     }
300                 } else {
301                     logger.info(
302                             "Command {} cannot be executed: UNDEF position or shutterRun configuration parameter not/wrongly set (thing={})",
303                             command, thing.getUID());
304                 }
305             }
306         } catch (OWNException e) {
307             logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
308         }
309     }
310
311     @Override
312     protected String ownIdPrefix() {
313         return Who.AUTOMATION.value().toString();
314     }
315
316     @Override
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);
323     }
324
325     /**
326      * Updates automation device state based on the Automation message received from
327      * OWN network
328      *
329      * @param msg the Automation message
330      */
331     private void updateAutomationState(Automation msg) {
332         logger.debug("updateAutomationState() - msg={} what={}", msg, msg.getWhat());
333         try {
334             if (msg.isCommandTranslation()) {
335                 logger.debug("msg is command translation, ignoring it");
336                 return;
337             }
338         } catch (FrameException fe) {
339             logger.warn("Exception while checking WHERE command translation for frame {}: {}, ignoring it", msg,
340                     fe.getMessage());
341         }
342         if (msg.isUp()) {
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);
347             }
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);
353             }
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;
373                 if (dw != null) {
374                     String w = dw.value();
375                     try {
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;
380                     }
381                 }
382             } else {
383                 updateMovingState(MOVING_STATE_STOPPED);
384                 // do deferred command, if present
385                 Command cmd = commandRequestedWhileMoving;
386                 if (cmd != null) {
387                     handleShutterCommand(cmd);
388                 }
389             }
390         } else {
391             logger.debug("Frame {} not supported for thing {}, ignoring it.", msg, thing.getUID());
392         }
393     }
394
395     /**
396      * Updates movingState to newState
397      */
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)));
405                 }
406             }
407         } else { // we were moving
408             updatePosition();
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)));
414                 }
415             }
416             // cancel the schedule
417             ScheduledFuture<?> mSc = moveSchedule;
418             if (mSc != null && !mSc.isDone()) {
419                 mSc.cancel(false);
420             }
421         }
422         movingState = newState;
423         logger.debug("# {} # movingState={} positionEstimation={} - calibrating={} shutterRun={}", deviceWhere,
424                 movingState, positionEstimation, calibrating, shutterRun);
425     }
426
427     /**
428      * Updates positionEstimation and then channel state based on movedTime and
429      * current movingState
430      */
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,
437                     movedTime);
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;
452                 }
453             }
454         }
455         if (newPos != POSITION_UNKNOWN) {
456             if (newPos != positionEstimation) {
457                 updateState(CHANNEL_SHUTTER, new PercentType(newPos));
458             }
459         } else {
460             updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
461         }
462         positionEstimation = newPos;
463     }
464
465     @Override
466     public void dispose() {
467         ScheduledFuture<?> mSc = moveSchedule;
468         if (mSc != null) {
469             mSc.cancel(true);
470         }
471         super.dispose();
472     }
473 }