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