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