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