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