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