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