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