]> git.basschouten.com Git - openhab-addons.git/blob
6b42bf7d01319c05d20edbbe37e3e1be831b5fb6
[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.hdpowerview.internal.handler;
14
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
16 import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
17
18 import java.util.Map;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import javax.ws.rs.NotSupportedException;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
27 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
28 import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
29 import org.openhab.binding.hdpowerview.internal.api.Firmware;
30 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
31 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
33 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
34 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
35 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
36 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
37 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
38 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
39 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.library.types.PercentType;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.types.StopMoveType;
45 import org.openhab.core.library.types.UpDownType;
46 import org.openhab.core.library.unit.Units;
47 import org.openhab.core.thing.Bridge;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.UnDefType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * Handles commands for an HD PowerView Shade
60  *
61  * @author Andy Lintner - Initial contribution
62  * @author Andrew Fiddian-Green - Added support for secondary rail positions
63  */
64 @NonNullByDefault
65 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
66
67     private enum RefreshKind {
68         POSITION,
69         SURVEY,
70         BATTERY_LEVEL
71     }
72
73     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
74     private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
75
76     private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
77     private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
78     private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
79     private @Nullable Capabilities capabilities;
80     private int shadeId;
81     private boolean isDisposing;
82
83     public HDPowerViewShadeHandler(Thing thing) {
84         super(thing);
85     }
86
87     @Override
88     public void initialize() {
89         logger.debug("Initializing shade handler");
90         isDisposing = false;
91         try {
92             shadeId = getShadeId();
93         } catch (NumberFormatException e) {
94             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95                     "@text/offline.conf-error.invalid-id");
96             return;
97         }
98         Bridge bridge = getBridge();
99         if (bridge == null) {
100             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
101             return;
102         }
103         if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
104             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
105                     "@text/offline.conf-error.invalid-bridge-handler");
106             return;
107         }
108         ThingStatus bridgeStatus = bridge.getStatus();
109         if (bridgeStatus == ThingStatus.ONLINE) {
110             updateStatus(ThingStatus.UNKNOWN);
111         } else {
112             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
113         }
114     }
115
116     @Override
117     public void dispose() {
118         logger.debug("Disposing shade handler for shade {}", shadeId);
119         isDisposing = true;
120         ScheduledFuture<?> future = refreshPositionFuture;
121         if (future != null) {
122             future.cancel(true);
123         }
124         refreshPositionFuture = null;
125         future = refreshSignalFuture;
126         if (future != null) {
127             future.cancel(true);
128         }
129         refreshSignalFuture = null;
130         future = refreshBatteryLevelFuture;
131         if (future != null) {
132             future.cancel(true);
133         }
134         refreshBatteryLevelFuture = null;
135         capabilities = null;
136     }
137
138     @Override
139     public void handleCommand(ChannelUID channelUID, Command command) {
140         String channelId = channelUID.getId();
141
142         if (RefreshType.REFRESH == command) {
143             switch (channelId) {
144                 case CHANNEL_SHADE_POSITION:
145                 case CHANNEL_SHADE_SECONDARY_POSITION:
146                 case CHANNEL_SHADE_VANE:
147                     requestRefreshShadePosition();
148                     break;
149                 case CHANNEL_SHADE_LOW_BATTERY:
150                 case CHANNEL_SHADE_BATTERY_LEVEL:
151                 case CHANNEL_SHADE_BATTERY_VOLTAGE:
152                     requestRefreshShadeBatteryLevel();
153                     break;
154                 case CHANNEL_SHADE_SIGNAL_STRENGTH:
155                     requestRefreshShadeSurvey();
156                     break;
157             }
158             return;
159         }
160
161         HDPowerViewHubHandler bridge = getBridgeHandler();
162         if (bridge == null) {
163             logger.warn("Missing bridge handler");
164             return;
165         }
166         HDPowerViewWebTargets webTargets = bridge.getWebTargets();
167         if (webTargets == null) {
168             logger.warn("Web targets not initialized");
169             return;
170         }
171         try {
172             handleShadeCommand(channelId, command, webTargets, shadeId);
173         } catch (HubInvalidResponseException e) {
174             Throwable cause = e.getCause();
175             if (cause == null) {
176                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
177             } else {
178                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
179             }
180         } catch (HubMaintenanceException e) {
181             // exceptions are logged in HDPowerViewWebTargets
182         } catch (HubException e) {
183             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
184             // for any ongoing requests. Logging this would only cause confusion.
185             if (!isDisposing) {
186                 logger.warn("Unexpected error: {}", e.getMessage());
187             }
188         }
189     }
190
191     private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
192             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
193         switch (channelId) {
194             case CHANNEL_SHADE_POSITION:
195                 if (command instanceof PercentType) {
196                     moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue(), webTargets, shadeId);
197                 } else if (command instanceof UpDownType) {
198                     moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
199                 } else if (command instanceof StopMoveType) {
200                     if (StopMoveType.STOP == command) {
201                         stopShade(webTargets, shadeId);
202                     } else {
203                         logger.warn("Unexpected StopMoveType command");
204                     }
205                 }
206                 break;
207
208             case CHANNEL_SHADE_VANE:
209                 if (command instanceof PercentType) {
210                     moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue(), webTargets, shadeId);
211                 } else if (command instanceof OnOffType) {
212                     moveShade(VANE_TILT_COORDS, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
213                 }
214                 break;
215
216             case CHANNEL_SHADE_SECONDARY_POSITION:
217                 if (command instanceof PercentType) {
218                     moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue(), webTargets, shadeId);
219                 } else if (command instanceof UpDownType) {
220                     moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
221                 } else if (command instanceof StopMoveType) {
222                     if (StopMoveType.STOP == command) {
223                         stopShade(webTargets, shadeId);
224                     } else {
225                         logger.warn("Unexpected StopMoveType command");
226                     }
227                 }
228                 break;
229
230             case CHANNEL_SHADE_CALIBRATE:
231                 if (OnOffType.ON == command) {
232                     calibrateShade(webTargets, shadeId);
233                 }
234                 break;
235         }
236     }
237
238     /**
239      * Update the state of the channels based on the ShadeData provided.
240      *
241      * @param shadeData the ShadeData to be used; may be null.
242      */
243     protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
244         if (shadeData != null) {
245             updateStatus(ThingStatus.ONLINE);
246             updateCapabilities(shadeData);
247             updateSoftProperties(shadeData);
248             updateFirmwareProperties(shadeData);
249             ShadePosition shadePosition = shadeData.positions;
250             if (shadePosition != null) {
251                 updatePositionStates(shadePosition);
252             }
253             updateBatteryLevelStates(shadeData.batteryStatus);
254             updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
255                     shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
256                             : UnDefType.UNDEF);
257             updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
258         } else {
259             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
260         }
261     }
262
263     private void updateCapabilities(ShadeData shade) {
264         if (capabilities != null) {
265             // Already cached.
266             return;
267         }
268         Integer value = shade.capabilities;
269         if (value != null) {
270             int valueAsInt = value.intValue();
271             logger.debug("Caching capabilities {} for shade {}", valueAsInt, shade.id);
272             capabilities = db.getCapabilities(valueAsInt);
273         } else {
274             logger.debug("Capabilities not included in shade response");
275         }
276     }
277
278     private Capabilities getCapabilitiesOrDefault() {
279         Capabilities capabilities = this.capabilities;
280         if (capabilities == null) {
281             return new Capabilities();
282         }
283         return capabilities;
284     }
285
286     /**
287      * Update the Thing's properties based on the contents of the provided ShadeData.
288      *
289      * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
290      * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
291      *
292      * @param shadeData
293      */
294     private void updateSoftProperties(ShadeData shadeData) {
295         final Map<String, String> properties = getThing().getProperties();
296         boolean propChanged = false;
297
298         // update 'type' property
299         final int type = shadeData.type;
300         String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
301         String propOldVal = properties.getOrDefault(propKey, "");
302         String propNewVal = db.getType(type).toString();
303         if (!propNewVal.equals(propOldVal)) {
304             propChanged = true;
305             getThing().setProperty(propKey, propNewVal);
306             if ((type > 0) && !db.isTypeInDatabase(type)) {
307                 db.logTypeNotInDatabase(type);
308             }
309         }
310
311         // update 'capabilities' property
312         final Integer temp = shadeData.capabilities;
313         final int capabilitiesVal = temp != null ? temp.intValue() : -1;
314         Capabilities capabilities = db.getCapabilities(capabilitiesVal);
315         propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
316         propOldVal = properties.getOrDefault(propKey, "");
317         propNewVal = capabilities.toString();
318         if (!propNewVal.equals(propOldVal)) {
319             propChanged = true;
320             getThing().setProperty(propKey, propNewVal);
321             if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
322                 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
323             }
324         }
325
326         if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
327                 && (capabilitiesVal != db.getType(type).getCapabilities())) {
328             db.logCapabilitiesMismatch(type, capabilitiesVal);
329         }
330     }
331
332     private void updateFirmwareProperties(ShadeData shadeData) {
333         Map<String, String> properties = editProperties();
334         Firmware shadeFirmware = shadeData.firmware;
335         Firmware motorFirmware = shadeData.motor;
336         if (shadeFirmware != null) {
337             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
338         }
339         if (motorFirmware != null) {
340             properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
341         }
342         updateProperties(properties);
343     }
344
345     /**
346      * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
347      *
348      * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
349      * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
350      * potential need to add support for that type resp. capabilities.
351      *
352      * @param shadeData
353      */
354     private void updateHardProperties(ShadeData shadeData) {
355         final ShadePosition positions = shadeData.positions;
356         if (positions == null) {
357             return;
358         }
359         Capabilities capabilities = getCapabilitiesOrDefault();
360         final Map<String, String> properties = getThing().getProperties();
361
362         // update 'secondary rail detected' property
363         String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
364         String propOldVal = properties.getOrDefault(propKey, "");
365         boolean propNewBool = positions.secondaryRailDetected();
366         String propNewVal = String.valueOf(propNewBool);
367         if (!propNewVal.equals(propOldVal)) {
368             getThing().setProperty(propKey, propNewVal);
369             if (propNewBool != capabilities.supportsSecondary()) {
370                 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
371             }
372         }
373
374         // update 'tilt anywhere detected' property
375         propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
376         propOldVal = properties.getOrDefault(propKey, "");
377         propNewBool = positions.tiltAnywhereDetected();
378         propNewVal = String.valueOf(propNewBool);
379         if (!propNewVal.equals(propOldVal)) {
380             getThing().setProperty(propKey, propNewVal);
381             if (propNewBool != capabilities.supportsTiltAnywhere()) {
382                 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
383             }
384         }
385     }
386
387     private void updatePositionStates(ShadePosition shadePos) {
388         Capabilities capabilities = this.capabilities;
389         if (capabilities == null) {
390             logger.debug("The 'shadeCapabilities' field has not yet been initialized");
391             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
392             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
393             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
394             return;
395         }
396         updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_ZERO_IS_CLOSED));
397         updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_COORDS));
398         updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_ZERO_IS_OPEN));
399     }
400
401     private void updateBatteryLevelStates(int batteryStatus) {
402         int mappedValue;
403         switch (batteryStatus) {
404             case 1: // Low
405                 mappedValue = 10;
406                 break;
407             case 2: // Medium
408                 mappedValue = 50;
409                 break;
410             case 3: // High
411             case 4: // Plugged in
412                 mappedValue = 100;
413                 break;
414             default: // No status available (0) or invalid
415                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
416                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
417                 return;
418         }
419         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
420         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
421     }
422
423     private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
424             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
425         ShadePosition newPosition = null;
426         // (try to) read the positions from the hub
427         ShadeData shadeData = webTargets.getShade(shadeId);
428         updateCapabilities(shadeData);
429         newPosition = shadeData.positions;
430         // if no positions returned, then create a new position
431         if (newPosition == null) {
432             newPosition = new ShadePosition();
433         }
434         Capabilities capabilities = getCapabilitiesOrDefault();
435         // set the new position value, and write the positions to the hub
436         shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
437         updateShadePositions(shadeData);
438     }
439
440     private void stopShade(HDPowerViewWebTargets webTargets, int shadeId)
441             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
442         updateShadePositions(webTargets.stopShade(shadeId));
443         // Positions in response from stop motion is not updated to to actual positions yet,
444         // so we need to request hard refresh.
445         requestRefreshShadePosition();
446     }
447
448     private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId)
449             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
450         updateShadePositions(webTargets.calibrateShade(shadeId));
451     }
452
453     private void updateShadePositions(ShadeData shadeData) {
454         ShadePosition shadePosition = shadeData.positions;
455         if (shadePosition == null) {
456             return;
457         }
458         updateCapabilities(shadeData);
459         updatePositionStates(shadePosition);
460     }
461
462     private int getShadeId() throws NumberFormatException {
463         String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
464         if (str == null) {
465             throw new NumberFormatException("null input string");
466         }
467         return Integer.parseInt(str);
468     }
469
470     /**
471      * Request that the shade shall undergo a 'hard' refresh for querying its current position
472      */
473     protected synchronized void requestRefreshShadePosition() {
474         if (refreshPositionFuture == null) {
475             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
476         }
477     }
478
479     /**
480      * Request that the shade shall undergo a 'hard' refresh for querying its survey data
481      */
482     protected synchronized void requestRefreshShadeSurvey() {
483         if (refreshSignalFuture == null) {
484             refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
485         }
486     }
487
488     /**
489      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
490      */
491     protected synchronized void requestRefreshShadeBatteryLevel() {
492         if (refreshBatteryLevelFuture == null) {
493             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
494         }
495     }
496
497     private void doRefreshShadePosition() {
498         this.doRefreshShade(RefreshKind.POSITION);
499         refreshPositionFuture = null;
500     }
501
502     private void doRefreshShadeSignal() {
503         this.doRefreshShade(RefreshKind.SURVEY);
504         refreshSignalFuture = null;
505     }
506
507     private void doRefreshShadeBatteryLevel() {
508         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
509         refreshBatteryLevelFuture = null;
510     }
511
512     private void doRefreshShade(RefreshKind kind) {
513         try {
514             HDPowerViewHubHandler bridge;
515             if ((bridge = getBridgeHandler()) == null) {
516                 throw new HubProcessingException("Missing bridge handler");
517             }
518             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
519             if (webTargets == null) {
520                 throw new HubProcessingException("Web targets not initialized");
521             }
522             ShadeData shadeData;
523             switch (kind) {
524                 case POSITION:
525                     shadeData = webTargets.refreshShadePosition(shadeId);
526                     break;
527                 case SURVEY:
528                     Survey survey = webTargets.getShadeSurvey(shadeId);
529                     if (survey.surveyData != null) {
530                         logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
531                     } else {
532                         logger.warn("No response from shade {} survey", shadeId);
533                     }
534                     return;
535                 case BATTERY_LEVEL:
536                     shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
537                     break;
538                 default:
539                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
540             }
541             if (Boolean.TRUE.equals(shadeData.timedOut)) {
542                 logger.warn("Shade {} wireless refresh time out", shadeId);
543             } else if (kind == RefreshKind.POSITION) {
544                 updateShadePositions(shadeData);
545                 updateHardProperties(shadeData);
546             }
547         } catch (HubInvalidResponseException e) {
548             Throwable cause = e.getCause();
549             if (cause == null) {
550                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
551             } else {
552                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
553             }
554         } catch (HubMaintenanceException e) {
555             // exceptions are logged in HDPowerViewWebTargets
556         } catch (HubException e) {
557             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
558             // for any ongoing requests. Logging this would only cause confusion.
559             if (!isDisposing) {
560                 logger.warn("Unexpected error: {}", e.getMessage());
561             }
562         }
563     }
564 }