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