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