]> git.basschouten.com Git - openhab-addons.git/blob
73b2093659dbdff787e8636b3e9559b995238d1b
[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 commands/messages for an Automation OpenWebNet
51  * device. It extends the abstract {@link OpenWebNetThingHandler}.
52  *
53  * @author Massimo Valla - Initial contribution
54  */
55 @NonNullByDefault
56 public class OpenWebNetAutomationHandler extends OpenWebNetThingHandler {
57
58     private final Logger logger = LoggerFactory.getLogger(OpenWebNetAutomationHandler.class);
59
60     private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("ss.SSS");
61
62     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.AUTOMATION_SUPPORTED_THING_TYPES;
63
64     private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
65
66     // moving states
67     public static final int MOVING_STATE_STOPPED = 0;
68     public static final int MOVING_STATE_MOVING_UP = 1;
69     public static final int MOVING_STATE_MOVING_DOWN = 2;
70     public static final int MOVING_STATE_UNKNOWN = -1;
71
72     // calibration states
73     public static final int CALIBRATION_INACTIVE = -1;
74     public static final int CALIBRATION_ACTIVATED = 0;
75     public static final int CALIBRATION_GOING_UP = 1;
76     public static final int CALIBRATION_GOING_DOWN = 2;
77
78     // positions
79     public static final int POSITION_MAX_STEPS = 100;
80     public static final int POSITION_DOWN = 100;
81     public static final int POSITION_UP = 0;
82     public static final int POSITION_UNKNOWN = -1;
83
84     public static final int SHUTTER_RUN_UNDEFINED = -1;
85     private int shutterRun = SHUTTER_RUN_UNDEFINED;
86     private static final String AUTO_CALIBRATION = "AUTO";
87
88     private long startedMovingAtTS = -1; // timestamp when device started moving UP/DOWN
89     private int movingState = MOVING_STATE_UNKNOWN;
90     private int positionEstimation = POSITION_UNKNOWN;
91     private @Nullable ScheduledFuture<?> moveSchedule;
92     private int positionRequested = POSITION_UNKNOWN;
93     private int calibrating = CALIBRATION_INACTIVE;
94     private static final int MIN_STEP_TIME_MSEC = 50;
95     private @Nullable Command commandRequestedWhileMoving = null;
96
97     public OpenWebNetAutomationHandler(Thing thing) {
98         super(thing);
99     }
100
101     @Override
102     public void initialize() {
103         super.initialize();
104         Object shutterRunConfig = getConfig().get(OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN);
105         try {
106             if (shutterRunConfig == null) {
107                 shutterRunConfig = AUTO_CALIBRATION;
108                 logger.debug("shutterRun null --> default to AUTO");
109             } else if (shutterRunConfig instanceof String) {
110                 if (AUTO_CALIBRATION.equalsIgnoreCase(((String) shutterRunConfig))) {
111                     logger.debug("shutterRun set to AUTO via configuration");
112                     shutterRun = SHUTTER_RUN_UNDEFINED; // reset shutterRun
113                 } else { // try to parse int>=1000
114                     int shutterRunInt = Integer.parseInt((String) shutterRunConfig);
115                     if (shutterRunInt < 1000) {
116                         throw new NumberFormatException();
117                     }
118                     shutterRun = shutterRunInt;
119                     logger.debug("shutterRun set to {} via configuration", shutterRun);
120                 }
121             } else {
122                 throw new NumberFormatException();
123             }
124         } catch (NumberFormatException e) {
125             logger.debug("Wrong configuration: {} setting must be {} or an integer >= 1000",
126                     OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN, AUTO_CALIBRATION);
127             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
128                     "@text/offline.wrong-configuration");
129             shutterRun = SHUTTER_RUN_UNDEFINED;
130         }
131         updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
132         positionEstimation = POSITION_UNKNOWN;
133     }
134
135     @Override
136     protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
137         return new WhereLightAutom(wStr);
138     }
139
140     @Override
141     protected void requestChannelState(ChannelUID channel) {
142         super.requestChannelState(channel);
143         Where w = deviceWhere;
144         if (w != null) {
145             try {
146                 send(Automation.requestStatus(w.value()));
147             } catch (OWNException e) {
148                 logger.debug("Exception while requesting state for channel {}: {} ", channel, e.getMessage());
149                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
150             }
151         }
152     }
153
154     @Override
155     protected long getRefreshAllLastTS() {
156         return lastAllDevicesRefreshTS;
157     };
158
159     @Override
160     protected void refreshDevice(boolean refreshAll) {
161         if (refreshAll) {
162             logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
163             try {
164                 send(Automation.requestStatus(WhereLightAutom.GENERAL.value()));
165                 lastAllDevicesRefreshTS = System.currentTimeMillis();
166             } catch (OWNException e) {
167                 logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
168             }
169         } else {
170             logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
171             requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_SHUTTER));
172         }
173     }
174
175     @Override
176     protected void handleChannelCommand(ChannelUID channel, Command command) {
177         switch (channel.getId()) {
178             case CHANNEL_SHUTTER:
179                 handleShutterCommand(command);
180                 break;
181             default: {
182                 logger.info("Unsupported channel UID {}", channel);
183             }
184         }
185     }
186
187     /**
188      * Handles Automation Roller shutter command (UP/DOWN, STOP/MOVE, PERCENT xx%)
189      */
190     private void handleShutterCommand(Command command) {
191         Where w = deviceWhere;
192         if (w != null) {
193             calibrating = CALIBRATION_INACTIVE; // cancel calibration if we receive a command
194             commandRequestedWhileMoving = null;
195             try {
196                 if (StopMoveType.STOP.equals(command)) {
197                     send(Automation.requestStop(w.value()));
198                 } else if (command instanceof UpDownType || command instanceof PercentType) {
199                     if (movingState == MOVING_STATE_MOVING_UP || movingState == MOVING_STATE_MOVING_DOWN) { // already
200                                                                                                             // moving
201                         logger.debug("# {} # already moving, STOP then defer command", deviceWhere);
202                         commandRequestedWhileMoving = command;
203                         sendHighPriority(Automation.requestStop(w.value()));
204                         return;
205                     } else {
206                         if (command instanceof UpDownType) {
207                             if (UpDownType.UP.equals(command)) {
208                                 send(Automation.requestMoveUp(w.value()));
209                             } else {
210                                 send(Automation.requestMoveDown(w.value()));
211                             }
212                         } else if (command instanceof PercentType) {
213                             handlePercentCommand((PercentType) command, w.value());
214                         }
215                     }
216                 } else {
217                     logger.debug("Unsupported command {} for thing {}", command, thing.getUID());
218                 }
219             } catch (OWNException e) {
220                 logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
221             }
222         }
223     }
224
225     /**
226      * Handles Automation PERCENT xx% command
227      */
228     private void handlePercentCommand(PercentType command, String w) {
229         int percent = command.intValue();
230         if (percent == positionEstimation) {
231             logger.debug("# {} # handlePercentCommand() Command {}% == positionEstimation -> nothing to do", w,
232                     percent);
233             return;
234         }
235         try {
236             if (percent == POSITION_DOWN) { // GO TO 100%
237                 send(Automation.requestMoveDown(w));
238             } else if (percent == POSITION_UP) { // GO TO 0%
239                 send(Automation.requestMoveUp(w));
240             } else { // GO TO XX%
241                 logger.debug("# {} # {}% requested", deviceWhere, percent);
242                 if (shutterRun == SHUTTER_RUN_UNDEFINED) {
243                     logger.debug("& {} & CALIBRATION - shutterRun not configured, starting CALIBRATION...",
244                             deviceWhere);
245                     calibrating = CALIBRATION_ACTIVATED;
246                     send(Automation.requestMoveUp(w));
247                     positionRequested = percent;
248                 } else if (shutterRun >= 1000 && positionEstimation != POSITION_UNKNOWN) {
249                     // these two must be known to calculate moveTime.
250                     // Calculate how much time we have to move and set a deadline to stop after that time
251                     int moveTime = Math
252                             .round(((float) Math.abs(percent - positionEstimation) / POSITION_MAX_STEPS * shutterRun));
253                     logger.debug("# {} # target moveTime={}", deviceWhere, moveTime);
254                     if (moveTime > MIN_STEP_TIME_MSEC) {
255                         ScheduledFuture<?> mSch = moveSchedule;
256                         if (mSch != null && !mSch.isDone()) {
257                             // a moveSchedule was already scheduled and is not done... let's cancel the schedule
258                             mSch.cancel(false);
259                             logger.debug("# {} # new XX% requested, old moveSchedule cancelled", deviceWhere);
260                         }
261                         // send a requestFirmwareVersion message to BUS gateways to wake up the CMD connection before
262                         // sending further cmds
263                         OpenWebNetBridgeHandler h = bridgeHandler;
264                         if (h != null && h.isBusGateway()) {
265                             OpenGateway gw = h.gateway;
266                             if (gw != null) {
267                                 if (!gw.isCmdConnectionReady()) {
268                                     logger.debug("# {} # waking-up CMD connection...", deviceWhere);
269                                     send(GatewayMgmt.requestFirmwareVersion());
270                                 }
271                             }
272                         }
273                         // REMINDER: start the schedule BEFORE sending the command, because the synch command waits for
274                         // ACK and can take some 300ms
275                         logger.debug("# {} # Starting schedule...", deviceWhere);
276                         moveSchedule = scheduler.schedule(() -> {
277                             logger.debug("# {} # moveSchedule expired, sending STOP...", deviceWhere);
278                             try {
279                                 sendHighPriority(Automation.requestStop(w));
280                             } catch (OWNException ex) {
281                                 logger.debug("Exception while sending request for command {}: {}", command,
282                                         ex.getMessage(), ex);
283                             }
284                         }, moveTime, TimeUnit.MILLISECONDS);
285                         logger.debug("# {} # ...schedule started, now sending highPriority command...", deviceWhere);
286                         if (percent < positionEstimation) {
287                             sendHighPriority(Automation.requestMoveUp(w));
288                         } else {
289                             sendHighPriority(Automation.requestMoveDown(w));
290                         }
291                         logger.debug("# {} # ...gateway.sendHighPriority() returned", deviceWhere);
292                     } else {
293                         logger.debug("# {} # moveTime <= MIN_STEP_TIME_MSEC ---> do nothing", deviceWhere);
294                     }
295                 } else {
296                     logger.info(
297                             "Command {} cannot be executed: unknown position or shutterRun configuration params not/wrongly set (thing={})",
298                             command, thing.getUID());
299                 }
300             }
301         } catch (OWNException e) {
302             logger.debug("Exception while sending request for command {}: {}", command, e.getMessage(), e);
303         }
304     }
305
306     @Override
307     protected String ownIdPrefix() {
308         return Who.AUTOMATION.value().toString();
309     }
310
311     @Override
312     protected void handleMessage(BaseOpenMessage msg) {
313         logger.debug("handleMessage({}) for thing: {}", msg, thing.getUID());
314         updateAutomationState((Automation) msg);
315         // REMINDER: update automation state, and only after update thing status using the super method, to avoid delays
316         super.handleMessage(msg);
317     }
318
319     /**
320      * Updates automation device state based on the Automation message received from OWN network
321      *
322      * @param msg the Automation message
323      */
324     private void updateAutomationState(Automation msg) {
325         logger.debug("updateAutomationState() - msg={} what={}", msg, msg.getWhat());
326         try {
327             if (msg.isCommandTranslation()) {
328                 logger.debug("msg is command translation, ignoring it");
329                 return;
330             }
331         } catch (FrameException fe) {
332             logger.warn("Exception while checking WHERE command translation for frame {}: {}, ignoring it", msg,
333                     fe.getMessage());
334         }
335         if (msg.isUp()) {
336             updateMovingState(MOVING_STATE_MOVING_UP);
337             if (calibrating == CALIBRATION_ACTIVATED) {
338                 calibrating = CALIBRATION_GOING_UP;
339                 logger.debug("& {} & CALIBRATION - started going ALL UP...", deviceWhere);
340             }
341         } else if (msg.isDown()) {
342             updateMovingState(MOVING_STATE_MOVING_DOWN);
343             if (calibrating == CALIBRATION_ACTIVATED) {
344                 calibrating = CALIBRATION_GOING_DOWN;
345                 logger.debug("& {} & CALIBRATION - started going ALL DOWN...", deviceWhere);
346             }
347         } else if (msg.isStop()) {
348             long stoppedAt = System.currentTimeMillis();
349             if (calibrating == CALIBRATION_GOING_DOWN && shutterRun == SHUTTER_RUN_UNDEFINED) {
350                 shutterRun = (int) (stoppedAt - startedMovingAtTS);
351                 logger.debug("& {} & CALIBRATION - reached DOWN ---> shutterRun={}", deviceWhere, shutterRun);
352                 updateMovingState(MOVING_STATE_STOPPED);
353                 logger.debug("& {} & CALIBRATION - COMPLETED, now going to {}%", deviceWhere, positionRequested);
354                 handleShutterCommand(new PercentType(positionRequested));
355                 Configuration configuration = editConfiguration();
356                 configuration.put(OpenWebNetBindingConstants.CONFIG_PROPERTY_SHUTTER_RUN, Integer.toString(shutterRun));
357                 updateConfiguration(configuration);
358                 logger.debug("& {} & CALIBRATION - configuration updated: shutterRun = {}ms", deviceWhere, shutterRun);
359             } else if (calibrating == CALIBRATION_GOING_UP) {
360                 updateMovingState(MOVING_STATE_STOPPED);
361                 logger.debug("& {} & CALIBRATION - reached UP, now sending DOWN command...", deviceWhere);
362                 calibrating = CALIBRATION_ACTIVATED;
363                 Where dw = deviceWhere;
364                 if (dw != null) {
365                     String w = dw.value();
366                     try {
367                         send(Automation.requestMoveDown(w));
368                     } catch (OWNException e) {
369                         logger.debug("Exception while sending DOWN command during calibration: {}", e.getMessage(), e);
370                         calibrating = CALIBRATION_INACTIVE;
371                     }
372                 }
373             } else {
374                 updateMovingState(MOVING_STATE_STOPPED);
375                 // do deferred command, if present
376                 Command cmd = commandRequestedWhileMoving;
377                 if (cmd != null) {
378                     handleShutterCommand(cmd);
379                 }
380             }
381         } else {
382             logger.debug("Frame {} not supported for thing {}, ignoring it.", msg, thing.getUID());
383         }
384     }
385
386     /**
387      * Updates movingState to newState
388      */
389     private void updateMovingState(int newState) {
390         if (movingState == MOVING_STATE_STOPPED) {
391             if (newState != MOVING_STATE_STOPPED) { // moving after stop
392                 startedMovingAtTS = System.currentTimeMillis();
393                 synchronized (DATE_FORMATTER) {
394                     logger.debug("# {} # MOVING {} - startedMovingAt={} - {}", deviceWhere, newState, startedMovingAtTS,
395                             DATE_FORMATTER.format(new Date(startedMovingAtTS)));
396                 }
397             }
398         } else { // we were moving
399             updatePosition();
400             if (newState != MOVING_STATE_STOPPED) { // moving after moving, take new timestamp
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             // cancel the schedule
408             ScheduledFuture<?> mSc = moveSchedule;
409             if (mSc != null && !mSc.isDone()) {
410                 mSc.cancel(false);
411             }
412         }
413         movingState = newState;
414         logger.debug("# {} # movingState={} positionEstimation={} - calibrating={} shutterRun={}", deviceWhere,
415                 movingState, positionEstimation, calibrating, shutterRun);
416     }
417
418     /**
419      * Updates positionEstimation and then channel state based on movedTime and current movingState
420      */
421     private void updatePosition() {
422         int newPos = POSITION_UNKNOWN;
423         if (shutterRun > 0 && startedMovingAtTS != -1) {// we have shutterRun and startedMovingAtTS defined, let's
424                                                         // calculate new positionEstimation
425             long movedTime = System.currentTimeMillis() - startedMovingAtTS;
426             logger.debug("# {} # current positionEstimation={} movedTime={}", deviceWhere, positionEstimation,
427                     movedTime);
428             int movedSteps = Math.round((float) movedTime / shutterRun * POSITION_MAX_STEPS);
429             logger.debug("# {} # movedSteps: {} {}", deviceWhere, movedSteps,
430                     (movingState == MOVING_STATE_MOVING_DOWN) ? "DOWN(+)" : "UP(-)");
431             if (positionEstimation == POSITION_UNKNOWN && movedSteps >= POSITION_MAX_STEPS) { // we did a full run
432                 newPos = (movingState == MOVING_STATE_MOVING_DOWN) ? POSITION_DOWN : POSITION_UP;
433             } else if (positionEstimation != POSITION_UNKNOWN) {
434                 newPos = positionEstimation
435                         + ((movingState == MOVING_STATE_MOVING_DOWN) ? movedSteps : -1 * movedSteps);
436                 logger.debug("# {} # {} {} {} = {}", deviceWhere, positionEstimation,
437                         (movingState == MOVING_STATE_MOVING_DOWN) ? "+" : "-", movedSteps, newPos);
438                 if (newPos > POSITION_DOWN) {
439                     newPos = POSITION_DOWN;
440                 } else if (newPos < POSITION_UP) {
441                     newPos = POSITION_UP;
442                 }
443             }
444         }
445         if (newPos != POSITION_UNKNOWN) {
446             if (newPos != positionEstimation) {
447                 updateState(CHANNEL_SHUTTER, new PercentType(newPos));
448             }
449         } else {
450             updateState(CHANNEL_SHUTTER, UnDefType.UNDEF);
451         }
452         positionEstimation = newPos;
453     }
454
455     @Override
456     public void dispose() {
457         ScheduledFuture<?> mSc = moveSchedule;
458         if (mSc != null) {
459             mSc.cancel(true);
460         }
461         super.dispose();
462     }
463 }