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