]> git.basschouten.com Git - openhab-addons.git/blob
d617bb06cfbeb3917aae692f5f9fb36a904ccf94
[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 java.util.ArrayList;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Map.Entry;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import javax.ws.rs.ProcessingException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
29 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
30 import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
31 import org.openhab.binding.hdpowerview.internal.HubProcessingException;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
36 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
37 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.thing.binding.builder.ChannelBuilder;
48 import org.openhab.core.thing.type.ChannelTypeUID;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.gson.JsonParseException;
55
56 /**
57  * The {@link HDPowerViewHubHandler} is responsible for handling commands, which
58  * are sent to one of the channels.
59  *
60  * @author Andy Lintner - Initial contribution
61  * @author Andrew Fiddian-Green - Added support for secondary rail positions
62  */
63 @NonNullByDefault
64 public class HDPowerViewHubHandler extends BaseBridgeHandler {
65
66     private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
67     private final HttpClient httpClient;
68
69     private long refreshInterval;
70     private long hardRefreshPositionInterval;
71     private long hardRefreshBatteryLevelInterval;
72
73     private @Nullable HDPowerViewWebTargets webTargets;
74     private @Nullable ScheduledFuture<?> pollFuture;
75     private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
76     private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
77
78     private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
79             HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
80
81     public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient) {
82         super(bridge);
83         this.httpClient = httpClient;
84     }
85
86     @Override
87     public void handleCommand(ChannelUID channelUID, Command command) {
88         if (RefreshType.REFRESH.equals(command)) {
89             requestRefreshShadePositions();
90             return;
91         }
92
93         Channel channel = getThing().getChannel(channelUID.getId());
94         if (channel != null && sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
95             if (OnOffType.ON.equals(command)) {
96                 try {
97                     HDPowerViewWebTargets webTargets = this.webTargets;
98                     if (webTargets == null) {
99                         throw new ProcessingException("Web targets not initialized");
100                     }
101                     webTargets.activateScene(Integer.parseInt(channelUID.getId()));
102                 } catch (HubMaintenanceException e) {
103                     // exceptions are logged in HDPowerViewWebTargets
104                 } catch (NumberFormatException | HubProcessingException e) {
105                     logger.debug("Unexpected error {}", e.getMessage());
106                 }
107             }
108         }
109     }
110
111     @Override
112     public void initialize() {
113         logger.debug("Initializing hub");
114         HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
115         String host = config.host;
116
117         if (host == null || host.isEmpty()) {
118             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host address must be set");
119             return;
120         }
121
122         webTargets = new HDPowerViewWebTargets(httpClient, host);
123         refreshInterval = config.refresh;
124         hardRefreshPositionInterval = config.hardRefresh;
125         hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
126         schedulePoll();
127     }
128
129     public @Nullable HDPowerViewWebTargets getWebTargets() {
130         return webTargets;
131     }
132
133     @Override
134     public void handleRemoval() {
135         super.handleRemoval();
136         stopPoll();
137     }
138
139     @Override
140     public void dispose() {
141         super.dispose();
142         stopPoll();
143     }
144
145     private void schedulePoll() {
146         ScheduledFuture<?> future = this.pollFuture;
147         if (future != null) {
148             future.cancel(false);
149         }
150         logger.debug("Scheduling poll for 5000ms out, then every {}ms", refreshInterval);
151         this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 5000, refreshInterval, TimeUnit.MILLISECONDS);
152
153         future = this.hardRefreshPositionFuture;
154         if (future != null) {
155             future.cancel(false);
156         }
157         if (hardRefreshPositionInterval > 0) {
158             logger.debug("Scheduling hard position refresh every {} minutes", hardRefreshPositionInterval);
159             this.hardRefreshPositionFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShadePositions, 1,
160                     hardRefreshPositionInterval, TimeUnit.MINUTES);
161         }
162
163         future = this.hardRefreshBatteryLevelFuture;
164         if (future != null) {
165             future.cancel(false);
166         }
167         if (hardRefreshBatteryLevelInterval > 0) {
168             logger.debug("Scheduling hard battery level refresh every {} hours", hardRefreshBatteryLevelInterval);
169             this.hardRefreshBatteryLevelFuture = scheduler.scheduleWithFixedDelay(
170                     this::requestRefreshShadeBatteryLevels, 1, hardRefreshBatteryLevelInterval, TimeUnit.HOURS);
171         }
172     }
173
174     private synchronized void stopPoll() {
175         ScheduledFuture<?> future = this.pollFuture;
176         if (future != null) {
177             future.cancel(true);
178         }
179         this.pollFuture = null;
180
181         future = this.hardRefreshPositionFuture;
182         if (future != null) {
183             future.cancel(true);
184         }
185         this.hardRefreshPositionFuture = null;
186
187         future = this.hardRefreshBatteryLevelFuture;
188         if (future != null) {
189             future.cancel(true);
190         }
191         this.hardRefreshBatteryLevelFuture = null;
192     }
193
194     private synchronized void poll() {
195         try {
196             logger.debug("Polling for state");
197             pollShades();
198             pollScenes();
199         } catch (JsonParseException e) {
200             logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
201         } catch (HubProcessingException e) {
202             logger.warn("Error connecting to bridge: {}", e.getMessage());
203             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
204         } catch (HubMaintenanceException e) {
205             // exceptions are logged in HDPowerViewWebTargets
206         }
207     }
208
209     private void pollShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
210         HDPowerViewWebTargets webTargets = this.webTargets;
211         if (webTargets == null) {
212             throw new ProcessingException("Web targets not initialized");
213         }
214
215         Shades shades = webTargets.getShades();
216         if (shades == null) {
217             throw new JsonParseException("Missing 'shades' element");
218         }
219
220         List<ShadeData> shadesData = shades.shadeData;
221         if (shadesData == null) {
222             throw new JsonParseException("Missing 'shades.shadeData' element");
223         }
224
225         updateStatus(ThingStatus.ONLINE);
226         logger.debug("Received data for {} shades", shadesData.size());
227
228         Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
229         Map<Thing, String> thingIdMap = getThingIdMap();
230         for (Entry<Thing, String> item : thingIdMap.entrySet()) {
231             Thing thing = item.getKey();
232             String shadeId = item.getValue();
233             ShadeData shadeData = idShadeDataMap.get(shadeId);
234             updateShadeThing(shadeId, thing, shadeData);
235         }
236     }
237
238     private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
239         HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
240         if (thingHandler == null) {
241             logger.debug("Shade '{}' handler not initialized", shadeId);
242             return;
243         }
244         if (shadeData == null) {
245             logger.debug("Shade '{}' has no data in hub", shadeId);
246         } else {
247             logger.debug("Updating shade '{}'", shadeId);
248         }
249         thingHandler.onReceiveUpdate(shadeData);
250     }
251
252     private void pollScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
253         HDPowerViewWebTargets webTargets = this.webTargets;
254         if (webTargets == null) {
255             throw new ProcessingException("Web targets not initialized");
256         }
257
258         Scenes scenes = webTargets.getScenes();
259         if (scenes == null) {
260             throw new JsonParseException("Missing 'scenes' element");
261         }
262
263         List<Scene> sceneData = scenes.sceneData;
264         if (sceneData == null) {
265             throw new JsonParseException("Missing 'scenes.sceneData' element");
266         }
267         logger.debug("Received data for {} scenes", sceneData.size());
268
269         Map<String, Channel> idChannelMap = getIdChannelMap();
270         for (Scene scene : sceneData) {
271             // remove existing scene channel from the map
272             String sceneId = Integer.toString(scene.id);
273             if (idChannelMap.containsKey(sceneId)) {
274                 idChannelMap.remove(sceneId);
275                 logger.debug("Keeping channel for existing scene '{}'", sceneId);
276             } else {
277                 // create a new scene channel
278                 ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneId);
279                 Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneChannelTypeUID)
280                         .withLabel(scene.getName()).withDescription("Activates the scene " + scene.getName()).build();
281                 updateThing(editThing().withChannel(channel).build());
282                 logger.debug("Creating new channel for scene '{}'", sceneId);
283             }
284         }
285
286         // remove any previously created channels that no longer exist
287         if (!idChannelMap.isEmpty()) {
288             logger.debug("Removing {} orphan scene channels", idChannelMap.size());
289             List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
290             allChannels.removeAll(idChannelMap.values());
291             updateThing(editThing().withChannels(allChannels).build());
292         }
293     }
294
295     private Map<Thing, String> getThingIdMap() {
296         Map<Thing, String> ret = new HashMap<>();
297         for (Thing thing : getThing().getThings()) {
298             String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
299             if (id != null && !id.isEmpty()) {
300                 ret.put(thing, id);
301             }
302         }
303         return ret;
304     }
305
306     private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
307         Map<String, ShadeData> ret = new HashMap<>();
308         for (ShadeData shade : shadeData) {
309             if (shade.id != 0) {
310                 ret.put(Integer.toString(shade.id), shade);
311             }
312         }
313         return ret;
314     }
315
316     private Map<String, Channel> getIdChannelMap() {
317         Map<String, Channel> ret = new HashMap<>();
318         for (Channel channel : getThing().getChannels()) {
319             if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
320                 ret.put(channel.getUID().getId(), channel);
321             }
322         }
323         return ret;
324     }
325
326     private void requestRefreshShadePositions() {
327         Map<Thing, String> thingIdMap = getThingIdMap();
328         for (Entry<Thing, String> item : thingIdMap.entrySet()) {
329             Thing thing = item.getKey();
330             ThingHandler handler = thing.getHandler();
331             if (handler instanceof HDPowerViewShadeHandler) {
332                 ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition();
333             } else {
334                 String shadeId = item.getValue();
335                 logger.debug("Shade '{}' handler not initialized", shadeId);
336             }
337         }
338     }
339
340     private void requestRefreshShadeBatteryLevels() {
341         Map<Thing, String> thingIdMap = getThingIdMap();
342         for (Entry<Thing, String> item : thingIdMap.entrySet()) {
343             Thing thing = item.getKey();
344             ThingHandler handler = thing.getHandler();
345             if (handler instanceof HDPowerViewShadeHandler) {
346                 ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel();
347             } else {
348                 String shadeId = item.getValue();
349                 logger.debug("Shade '{}' handler not initialized", shadeId);
350             }
351         }
352     }
353 }