]> git.basschouten.com Git - openhab-addons.git/blob
404400345874b7f7e0bd138555e6e2618917d5a3
[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.hydrawise.internal;
14
15 import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
16
17 import java.time.ZonedDateTime;
18 import java.time.temporal.ChronoUnit;
19 import java.util.*;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import org.apache.commons.lang.StringUtils;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
27 import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
28 import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
29 import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
30 import org.openhab.binding.hydrawise.internal.api.model.Relay;
31 import org.openhab.binding.hydrawise.internal.api.model.Running;
32 import org.openhab.core.library.types.DateTimeType;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.openhab.core.types.State;
44 import org.openhab.core.types.UnDefType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * The {@link HydrawiseHandler} is responsible for handling commands, which are
50  * sent to one of the channels.
51  *
52  * @author Dan Cunningham - Initial contribution
53  */
54 @NonNullByDefault
55 public abstract class HydrawiseHandler extends BaseThingHandler {
56
57     private final Logger logger = LoggerFactory.getLogger(HydrawiseHandler.class);
58     private @Nullable ScheduledFuture<?> pollFuture;
59     private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
60     private Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
61
62     /**
63      * value observed being used by the Hydrawise clients as a max time value,
64      */
65     private static long MAX_RUN_TIME = 157680000;
66
67     /**
68      * Minimum amount of time we can poll for updates
69      */
70     protected static final int MIN_REFRESH_SECONDS = 5;
71
72     /**
73      * Minimum amount of time we can poll after a command
74      */
75     protected static final int COMMAND_REFRESH_SECONDS = 5;
76
77     /**
78      * Our poll rate
79      */
80     protected int refresh;
81
82     /**
83      * Future to poll for updated
84      */
85
86     public HydrawiseHandler(Thing thing) {
87         super(thing);
88     }
89
90     @SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
91     @Override
92     public void handleCommand(ChannelUID channelUID, Command command) {
93         if (getThing().getStatus() != ThingStatus.ONLINE) {
94             logger.warn("Controller is NOT ONLINE and is not responding to commands");
95             return;
96         }
97
98         // remove our cached state for this, will be safely updated on next poll
99         stateMap.remove(channelUID.getAsString());
100
101         if (command instanceof RefreshType) {
102             // we already removed this from the cache
103             return;
104         }
105
106         String group = channelUID.getGroupId();
107         String channelId = channelUID.getIdWithoutGroup();
108         boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
109
110         Relay relay = relayMap.get(group);
111         if (!allCommand && relay == null) {
112             logger.debug("Zone not found {}", group);
113             return;
114         }
115
116         try {
117             clearPolling();
118             switch (channelId) {
119                 case CHANNEL_ZONE_RUN_CUSTOM:
120                     if (!(command instanceof DecimalType)) {
121                         logger.warn("Invalid command type for run custom {}", command.getClass().getName());
122                         return;
123                     }
124                     if (allCommand) {
125                         sendRunAllCommand(((DecimalType) command).intValue());
126                     } else {
127                         Objects.requireNonNull(relay);
128                         sendRunCommand(((DecimalType) command).intValue(), relay);
129                     }
130                     break;
131                 case CHANNEL_ZONE_RUN:
132                     if (!(command instanceof OnOffType)) {
133                         logger.warn("Invalid command type for run {}", command.getClass().getName());
134                         return;
135                     }
136                     if (allCommand) {
137                         if (command == OnOffType.ON) {
138                             sendRunAllCommand();
139                         } else {
140                             sendStopAllCommand();
141                         }
142                     } else {
143                         Objects.requireNonNull(relay);
144                         if (command == OnOffType.ON) {
145                             sendRunCommand(relay);
146                         } else {
147                             sendStopCommand(relay);
148                         }
149                     }
150                     break;
151             }
152             initPolling(COMMAND_REFRESH_SECONDS);
153         } catch (HydrawiseCommandException | HydrawiseConnectionException e) {
154             logger.debug("Could not issue command", e);
155             initPolling(COMMAND_REFRESH_SECONDS);
156         } catch (HydrawiseAuthenticationException e) {
157             logger.debug("Credentials not valid");
158             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
159             configureInternal();
160         }
161     }
162
163     @Override
164     public void initialize() {
165         scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
166     }
167
168     @Override
169     public void dispose() {
170         logger.debug("Handler disposed.");
171         clearPolling();
172     }
173
174     @Override
175     public void channelLinked(ChannelUID channelUID) {
176         // clear our cached value so the new channel gets updated on the next poll
177         stateMap.remove(channelUID.getId());
178     }
179
180     protected abstract void configure()
181             throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException;
182
183     protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException;
184
185     protected abstract void sendRunCommand(int seconds, Relay relay)
186             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
187
188     protected abstract void sendRunCommand(Relay relay)
189             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
190
191     protected abstract void sendStopCommand(Relay relay)
192             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
193
194     protected abstract void sendRunAllCommand()
195             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
196
197     protected abstract void sendRunAllCommand(int seconds)
198             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
199
200     protected abstract void sendStopAllCommand()
201             throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
202
203     protected void updateZones(LocalScheduleResponse status) {
204         ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
205         status.relays.forEach(r -> {
206             String group = "zone" + r.getRelayNumber();
207             relayMap.put(group, r);
208             logger.trace("Updateing Zone {} {} ", group, r.name);
209             updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
210             updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
211             updateGroupState(group, CHANNEL_ZONE_TIME,
212                     r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF);
213             if (StringUtils.isNotBlank(r.icon)) {
214                 updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + r.icon));
215             }
216             if (r.time >= MAX_RUN_TIME) {
217                 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
218             } else {
219                 updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
220                         new DateTimeType(now.plusSeconds(r.time).truncatedTo(ChronoUnit.MINUTES)));
221             }
222
223             Optional<Running> running = status.running.stream()
224                     .filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
225             if (running.isPresent()) {
226                 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
227                 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft));
228                 logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
229
230             } else {
231                 updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
232                 updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0));
233
234             }
235
236             updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
237                     !status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
238         });
239     }
240
241     protected void updateGroupState(String group, String channelID, State state) {
242         String channelName = group + "#" + channelID;
243         State oldState = stateMap.put(channelName, state);
244         if (!state.equals(oldState)) {
245             ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
246             logger.debug("updateState updating {} {}", channelUID, state);
247             updateState(channelUID, state);
248         }
249     }
250
251     @SuppressWarnings("serial")
252     @NonNullByDefault
253     protected class NotConfiguredException extends Exception {
254         NotConfiguredException(String message) {
255             super(message);
256         }
257     }
258
259     private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
260         return future != null && !future.isCancelled();
261     }
262
263     private void configureInternal() {
264         clearPolling();
265         stateMap.clear();
266         relayMap.clear();
267         try {
268             configure();
269             initPolling(0);
270         } catch (NotConfiguredException e) {
271             logger.debug("Configuration error {}", e.getMessage());
272             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
273         } catch (HydrawiseConnectionException e) {
274             logger.debug("Could not connect to service");
275             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
276         } catch (HydrawiseAuthenticationException e) {
277             logger.debug("Credentials not valid");
278             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Credentials not valid");
279         }
280     }
281
282     /**
283      * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
284      * and we need to poll sooner then the next refresh cycle.
285      */
286     private synchronized void initPolling(int initalDelay) {
287         clearPolling();
288         pollFuture = scheduler.scheduleWithFixedDelay(this::pollControllerInternal, initalDelay, refresh,
289                 TimeUnit.SECONDS);
290     }
291
292     /**
293      * Stops/clears this thing's polling future
294      */
295     private void clearPolling() {
296         ScheduledFuture<?> localFuture = pollFuture;
297         if (isFutureValid(localFuture)) {
298             if (localFuture != null) {
299                 localFuture.cancel(false);
300             }
301         }
302     }
303
304     /**
305      * Poll the controller for updates.
306      */
307     private void pollControllerInternal() {
308         try {
309             pollController();
310             if (getThing().getStatus() != ThingStatus.ONLINE) {
311                 updateStatus(ThingStatus.ONLINE);
312             }
313         } catch (HydrawiseConnectionException e) {
314             // poller will continue to run, set offline until next run
315             logger.debug("Exception polling", e);
316             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
317         } catch (HydrawiseAuthenticationException e) {
318             // if are creds are not valid, we need to try re authorizing again
319             logger.debug("Authorization exception during polling", e);
320             configureInternal();
321         }
322     }
323 }