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