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