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