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