]> git.basschouten.com Git - openhab-addons.git/blob
26ae2389d4377a70d3ee22d4c288a06d4e246e4a
[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.orbitbhyve.internal.handler;
14
15 import static org.openhab.binding.orbitbhyve.internal.OrbitBhyveBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.text.SimpleDateFormat;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Date;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.TimeoutException;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.StringContentProvider;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.websocket.api.Session;
41 import org.eclipse.jetty.websocket.api.WebSocketException;
42 import org.eclipse.jetty.websocket.client.WebSocketClient;
43 import org.openhab.binding.orbitbhyve.internal.OrbitBhyveConfiguration;
44 import org.openhab.binding.orbitbhyve.internal.discovery.OrbitBhyveDiscoveryService;
45 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveDevice;
46 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveProgram;
47 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveSessionResponse;
48 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveSocketEvent;
49 import org.openhab.binding.orbitbhyve.internal.net.OrbitBhyveSocket;
50 import org.openhab.core.config.core.status.ConfigStatusMessage;
51 import org.openhab.core.library.types.OnOffType;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.Channel;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
60 import org.openhab.core.thing.binding.ThingHandler;
61 import org.openhab.core.thing.binding.ThingHandlerService;
62 import org.openhab.core.types.Command;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 import com.google.gson.Gson;
67 import com.google.gson.JsonSyntaxException;
68
69 /**
70  * The {@link OrbitBhyveBridgeHandler} is responsible for handling commands, which are
71  * sent to one of the channels.
72  *
73  * @author Ondrej Pecta - Initial contribution
74  */
75 @NonNullByDefault
76 public class OrbitBhyveBridgeHandler extends ConfigStatusBridgeHandler {
77
78     private final Logger logger = LoggerFactory.getLogger(OrbitBhyveBridgeHandler.class);
79
80     private final HttpClient httpClient;
81
82     private final WebSocketClient webSocketClient;
83
84     private @Nullable ScheduledFuture<?> future = null;
85
86     private @Nullable Session session;
87
88     private @Nullable String sessionToken = null;
89
90     private OrbitBhyveConfiguration config = new OrbitBhyveConfiguration();
91
92     private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
93
94     // Gson & parser
95     private final Gson gson = new Gson();
96
97     public OrbitBhyveBridgeHandler(Bridge thing, HttpClient httpClient, WebSocketClient webSocketClient) {
98         super(thing);
99         this.httpClient = httpClient;
100         this.webSocketClient = webSocketClient;
101     }
102
103     @Override
104     public Collection<ConfigStatusMessage> getConfigStatus() {
105         return Collections.emptyList();
106     }
107
108     @Override
109     public void handleCommand(ChannelUID channelUID, Command command) {
110     }
111
112     @Override
113     public Collection<Class<? extends ThingHandlerService>> getServices() {
114         return Set.of(OrbitBhyveDiscoveryService.class);
115     }
116
117     @Override
118     public void initialize() {
119         config = getConfigAs(OrbitBhyveConfiguration.class);
120         httpClient.setFollowRedirects(false);
121
122         scheduler.execute(() -> {
123             login();
124             future = scheduler.scheduleWithFixedDelay(this::ping, 0, config.refresh, TimeUnit.SECONDS);
125         });
126         logger.debug("Finished initializing!");
127     }
128
129     @Override
130     public void dispose() {
131         ScheduledFuture<?> localFuture = future;
132         if (localFuture != null) {
133             localFuture.cancel(true);
134         }
135         closeSession();
136         super.dispose();
137     }
138
139     private boolean login() {
140         try {
141             String urlParameters = "{\"session\":{\"email\":\"" + config.email + "\",\"password\":\"" + config.password
142                     + "\"}}";
143             ContentResponse response = httpClient.newRequest(BHYVE_SESSION).method(HttpMethod.POST).agent(AGENT)
144                     .content(new StringContentProvider(urlParameters), "application/json; charset=utf-8")
145                     .timeout(BHYVE_TIMEOUT, TimeUnit.SECONDS).send();
146             if (response.getStatus() == 200) {
147                 if (logger.isTraceEnabled()) {
148                     logger.trace("response: {}", response.getContentAsString());
149                 }
150                 OrbitBhyveSessionResponse session = gson.fromJson(response.getContentAsString(),
151                         OrbitBhyveSessionResponse.class);
152                 sessionToken = session.getOrbitSessionToken();
153                 logger.debug("token: {}", sessionToken);
154                 initializeWebSocketSession();
155             } else {
156                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157                         "Login response status:" + response.getStatus());
158                 return false;
159             }
160         } catch (TimeoutException | ExecutionException e) {
161             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
162             return false;
163         } catch (InterruptedException e) {
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
165             Thread.currentThread().interrupt();
166             return false;
167         }
168         updateStatus(ThingStatus.ONLINE);
169         return true;
170     }
171
172     private synchronized void ping() {
173         if (ThingStatus.OFFLINE == thing.getStatus()) {
174             login();
175         }
176
177         if (ThingStatus.ONLINE == thing.getStatus()) {
178             Session localSession = session;
179             if (localSession == null || !localSession.isOpen()) {
180                 initializeWebSocketSession();
181             }
182             localSession = session;
183             if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
184                 try {
185                     logger.debug("Sending ping");
186                     localSession.getRemote().sendString("{\"event\":\"ping\"}");
187                     updateAllStatuses();
188                 } catch (IOException e) {
189                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
190                             "Error sending ping (IOException on web socket)");
191                 } catch (WebSocketException e) {
192                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193                             String.format("Error sending ping (WebSocketException: %s)", e.getMessage()));
194                 }
195             } else {
196                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Web socket creation error");
197             }
198         }
199     }
200
201     public List<OrbitBhyveDevice> getDevices() {
202         try {
203             ContentResponse response = sendRequestBuilder(BHYVE_DEVICES, HttpMethod.GET).send();
204             if (response.getStatus() == 200) {
205                 if (logger.isTraceEnabled()) {
206                     logger.trace("Devices response: {}", response.getContentAsString());
207                 }
208                 OrbitBhyveDevice[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveDevice[].class);
209                 return Arrays.asList(devices);
210             } else {
211                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
212                         "Get devices returned response status: " + response.getStatus());
213             }
214         } catch (JsonSyntaxException e) {
215             logger.debug("Exception parsing devices json: {}", e.getMessage(), e);
216             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error parsing devices json");
217         } catch (TimeoutException | ExecutionException e) {
218             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting devices");
219         } catch (InterruptedException e) {
220             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting devices");
221             Thread.currentThread().interrupt();
222         }
223         return new ArrayList<>();
224     }
225
226     Request sendRequestBuilder(String uri, HttpMethod method) {
227         return httpClient.newRequest(uri).method(method).agent(AGENT).header("Orbit-Session-Token", sessionToken)
228                 .timeout(BHYVE_TIMEOUT, TimeUnit.SECONDS);
229     }
230
231     public @Nullable OrbitBhyveDevice getDevice(String deviceId) {
232         try {
233             ContentResponse response = sendRequestBuilder(BHYVE_DEVICES + "/" + deviceId, HttpMethod.GET).send();
234             if (response.getStatus() == 200) {
235                 if (logger.isTraceEnabled()) {
236                     logger.trace("Device response: {}", response.getContentAsString());
237                 }
238                 return gson.fromJson(response.getContentAsString(), OrbitBhyveDevice.class);
239             } else {
240                 logger.debug("Returned status: {}", response.getStatus());
241                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
242                         "Returned status: " + response.getStatus());
243             }
244         } catch (JsonSyntaxException e) {
245             logger.debug("Exception parsing device json: {}", e.getMessage(), e);
246             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error parsing device json");
247         } catch (TimeoutException | ExecutionException e) {
248             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
249                     "Error during getting device info: " + deviceId);
250         } catch (InterruptedException e) {
251             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
252                     "Error during getting device info: " + deviceId);
253             Thread.currentThread().interrupt();
254         }
255         return null;
256     }
257
258     public synchronized void processStatusResponse(String content) {
259         updateStatus(ThingStatus.ONLINE);
260         logger.trace("Got message: {}", content);
261         OrbitBhyveSocketEvent event = gson.fromJson(content, OrbitBhyveSocketEvent.class);
262         if (event != null) {
263             processEvent(event);
264         }
265     }
266
267     private void processEvent(OrbitBhyveSocketEvent event) {
268         switch (event.getEvent()) {
269             case "watering_in_progress_notification":
270                 disableZones(event.getDeviceId());
271                 Channel channel = getThingChannel(event.getDeviceId(), event.getStation());
272                 if (channel != null) {
273                     logger.debug("Watering zone: {}", event.getStation());
274                     updateState(channel.getUID(), OnOffType.ON);
275                     String program = event.getProgram().getAsString();
276                     if (!program.isEmpty() && !"manual".equals(program)) {
277                         channel = getThingChannel(event.getDeviceId(), "program_" + program);
278                         if (channel != null) {
279                             updateState(channel.getUID(), OnOffType.ON);
280                         }
281                     }
282                 }
283                 break;
284             case "watering_complete":
285                 logger.debug("Watering complete");
286                 disableZones(event.getDeviceId());
287                 disablePrograms(event.getDeviceId());
288                 updateDeviceStatus(event.getDeviceId());
289                 break;
290             case "change_mode":
291                 logger.debug("Updating mode to: {}", event.getMode());
292                 Channel ch = getThingChannel(event.getDeviceId(), CHANNEL_MODE);
293                 if (ch != null) {
294                     updateState(ch.getUID(), new StringType(event.getMode()));
295                 }
296                 ch = getThingChannel(event.getDeviceId(), CHANNEL_CONTROL);
297                 if (ch != null) {
298                     updateState(ch.getUID(), OnOffType.from(!"off".equals(event.getMode())));
299                 }
300                 updateDeviceStatus(event.getDeviceId());
301                 break;
302             case "rain_delay":
303                 updateDeviceStatus(event.getDeviceId());
304                 break;
305             case "skip_active_station":
306                 disableZones(event.getDeviceId());
307                 break;
308             case "program_changed":
309                 OrbitBhyveProgram program = gson.fromJson(event.getProgram(), OrbitBhyveProgram.class);
310                 if (program != null) {
311                     updateDeviceProgramStatus(program);
312                     updateDeviceStatus(program.getDeviceId());
313                 }
314                 break;
315             default:
316                 logger.debug("Received event: {}", event.getEvent());
317         }
318     }
319
320     private void updateAllStatuses() {
321         List<OrbitBhyveDevice> devices = getDevices();
322         for (Thing th : getThing().getThings()) {
323             if (th.isEnabled()) {
324                 String deviceId = th.getUID().getId();
325                 ThingHandler handler = th.getHandler();
326                 if (handler instanceof OrbitBhyveSprinklerHandler sprinklerHandler) {
327                     for (OrbitBhyveDevice device : devices) {
328                         if (deviceId.equals(th.getUID().getId())) {
329                             updateDeviceStatus(device, sprinklerHandler);
330                         }
331                     }
332                 }
333             }
334         }
335     }
336
337     private void updateDeviceStatus(@Nullable OrbitBhyveDevice device, @Nullable OrbitBhyveSprinklerHandler handler) {
338         if (device != null && handler != null) {
339             handler.setDeviceOnline(device.isConnected());
340             handler.updateDeviceStatus(device.getStatus());
341             handler.updateSmartWatering(device.getWaterSenseMode());
342             return;
343         }
344     }
345
346     private void updateDeviceStatus(String deviceId) {
347         for (Thing th : getThing().getThings()) {
348             if (deviceId.equals(th.getUID().getId())) {
349                 ThingHandler handler = th.getHandler();
350                 if (handler instanceof OrbitBhyveSprinklerHandler sprinklerHandler) {
351                     OrbitBhyveDevice device = getDevice(deviceId);
352                     updateDeviceStatus(device, sprinklerHandler);
353                 }
354             }
355         }
356     }
357
358     private void updateDeviceProgramStatus(OrbitBhyveProgram program) {
359         for (Thing th : getThing().getThings()) {
360             if (program.getDeviceId().equals(th.getUID().getId())) {
361                 ThingHandler handler = th.getHandler();
362                 if (handler instanceof OrbitBhyveSprinklerHandler sprinklerHandler) {
363                     sprinklerHandler.updateProgram(program);
364                 }
365             }
366         }
367     }
368
369     private void disableZones(String deviceId) {
370         disableChannel(deviceId, "zone_");
371     }
372
373     private void disablePrograms(String deviceId) {
374         disableChannel(deviceId, "program_");
375     }
376
377     private void disableChannel(String deviceId, String name) {
378         for (Thing th : getThing().getThings()) {
379             if (deviceId.equals(th.getUID().getId())) {
380                 for (Channel ch : th.getChannels()) {
381                     if (ch.getUID().getId().startsWith(name)) {
382                         updateState(ch.getUID(), OnOffType.OFF);
383                     }
384                 }
385                 return;
386             }
387         }
388     }
389
390     private @Nullable Channel getThingChannel(String deviceId, int station) {
391         for (Thing th : getThing().getThings()) {
392             if (deviceId.equals(th.getUID().getId())) {
393                 return th.getChannel("zone_" + station);
394             }
395         }
396         logger.debug("Cannot find zone: {} for device: {}", station, deviceId);
397         return null;
398     }
399
400     private @Nullable Channel getThingChannel(String deviceId, String name) {
401         for (Thing th : getThing().getThings()) {
402             if (deviceId.equals(th.getUID().getId())) {
403                 return th.getChannel(name);
404             }
405         }
406         logger.debug("Cannot find channel: {} for device: {}", name, deviceId);
407         return null;
408     }
409
410     private @Nullable Session createSession() {
411         String url = BHYVE_WS_URL;
412         URI uri = URI.create(url);
413
414         try {
415             // The socket that receives events
416             OrbitBhyveSocket socket = new OrbitBhyveSocket(this);
417             // Attempt Connect
418             Future<Session> fut = webSocketClient.connect(socket, uri);
419             // Wait for Connect
420             return fut.get();
421         } catch (IOException e) {
422             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot connect websocket client");
423         } catch (InterruptedException e) {
424             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
425             Thread.currentThread().interrupt();
426         } catch (ExecutionException e) {
427             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
428         }
429         return null;
430     }
431
432     private synchronized void initializeWebSocketSession() {
433         logger.debug("Initializing WebSocket session");
434         closeSession();
435         session = createSession();
436         Session localSession = session;
437         if (localSession != null) {
438             logger.debug("WebSocket connected!");
439             try {
440                 String msg = "{\"event\":\"app_connection\",\"orbit_session_token\":\"" + sessionToken + "\"}";
441                 logger.trace("sending message:\n {}", msg);
442                 localSession.getRemote().sendString(msg);
443             } catch (IOException e) {
444                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
445                         "Error sending hello string (IOException on web socket)");
446             } catch (WebSocketException e) {
447                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
448                         String.format("Error sending hello string (WebSocketException: %s)", e.getMessage()));
449             }
450         }
451     }
452
453     private void closeSession() {
454         Session localSession = session;
455         if (localSession != null && localSession.isOpen()) {
456             localSession.close();
457         }
458     }
459
460     public void runZone(String deviceId, String zone, int time) {
461         String dateTime = format.format(new Date());
462         try {
463             ping();
464             Session localSession = session;
465             if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
466                 localSession.getRemote()
467                         .sendString("{\"event\":\"change_mode\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\""
468                                 + dateTime + "\",\"mode\":\"manual\",\"stations\":[{\"station\":" + zone
469                                 + ",\"run_time\":" + time + "}]}");
470             }
471         } catch (IOException e) {
472             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
473                     "Error during zone watering execution");
474         }
475     }
476
477     public void runProgram(String deviceId, String program) {
478         String dateTime = format.format(new Date());
479         try {
480             ping();
481             Session localSession = session;
482             if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
483                 localSession.getRemote().sendString("{\"event\":\"change_mode\",\"mode\":\"manual\",\"program\":\""
484                         + program + "\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\"" + dateTime + "\"}");
485             }
486         } catch (IOException e) {
487             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
488                     "Error sending program watering execution (IOException on web socket)");
489         } catch (WebSocketException e) {
490             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
491                     String.format("Error sending program watering execution (WebSocketException: %s)", e.getMessage()));
492         }
493     }
494
495     public void enableProgram(OrbitBhyveProgram program, boolean enable) {
496         try {
497             String payLoad = "{\"sprinkler_timer_program\":{\"id\":\"" + program.getId() + "\",\"device_id\":\""
498                     + program.getDeviceId() + "\",\"program\":\"" + program.getProgram() + "\",\"enabled\":" + enable
499                     + "}}";
500             logger.debug("updating program {} with data {}", program.getProgram(), payLoad);
501             ContentResponse response = sendRequestBuilder(BHYVE_PROGRAMS + "/" + program.getId(), HttpMethod.PUT)
502                     .content(new StringContentProvider(payLoad), "application/json; charset=utf-8").send();
503             if (response.getStatus() == 200) {
504                 if (logger.isTraceEnabled()) {
505                     logger.trace("Enable programs response: {}", response.getContentAsString());
506                 }
507                 return;
508             } else {
509                 logger.debug("Returned status: {}", response.getStatus());
510                 updateStatus(ThingStatus.OFFLINE);
511             }
512         } catch (TimeoutException | ExecutionException e) {
513             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating programs");
514         } catch (InterruptedException e) {
515             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating programs");
516             Thread.currentThread().interrupt();
517         }
518     }
519
520     public void setRainDelay(String deviceId, int delay) {
521         String dateTime = format.format(new Date());
522         try {
523             ping();
524             Session localSession = session;
525             if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
526                 localSession.getRemote().sendString("{\"event\":\"rain_delay\",\"device_id\":\"" + deviceId
527                         + "\",\"delay\":" + delay + ",\"timestamp\":\"" + dateTime + "\"}");
528             }
529         } catch (IOException e) {
530             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
531                     "Error setting rain delay (IOException on web socket)");
532         } catch (WebSocketException e) {
533             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
534                     String.format("Error setting rain delay (WebSocketException: %s)", e.getMessage()));
535         }
536     }
537
538     public void stopWatering(String deviceId) {
539         String dateTime = format.format(new Date());
540         try {
541             ping();
542             Session localSession = session;
543             if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
544                 localSession.getRemote().sendString("{\"event\":\"change_mode\",\"device_id\":\"" + deviceId
545                         + "\",\"timestamp\":\"" + dateTime + "\",\"mode\":\"manual\",\"stations\":[]}");
546             }
547         } catch (IOException e) {
548             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
549                     "Error sending stop watering (IOException on web socket)");
550         } catch (WebSocketException e) {
551             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
552                     String.format("Error sending stop watering (WebSocketException: %s)", e.getMessage()));
553         }
554     }
555
556     public List<OrbitBhyveProgram> getPrograms() {
557         try {
558             ContentResponse response = sendRequestBuilder(BHYVE_PROGRAMS, HttpMethod.GET).send();
559             if (response.getStatus() == 200) {
560                 if (logger.isTraceEnabled()) {
561                     logger.trace("Programs response: {}", response.getContentAsString());
562                 }
563                 OrbitBhyveProgram[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveProgram[].class);
564                 return Arrays.asList(devices);
565             } else {
566                 logger.debug("Returned status: {}", response.getStatus());
567                 updateStatus(ThingStatus.OFFLINE);
568             }
569         } catch (TimeoutException | ExecutionException e) {
570             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs");
571         } catch (InterruptedException e) {
572             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs");
573             Thread.currentThread().interrupt();
574         }
575         return new ArrayList<>();
576     }
577
578     public void changeRunMode(String deviceId, String mode) {
579         String dateTime = format.format(new Date());
580         try {
581             ping();
582             Session localSession = session;
583             if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
584                 localSession.getRemote().sendString("{\"event\":\"change_mode\",\"mode\":\"" + mode
585                         + "\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\"" + dateTime + "\"}");
586             }
587         } catch (IOException e) {
588             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
589                     "Error setting run mode (IOException on web socket)");
590         } catch (WebSocketException e) {
591             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
592                     String.format("Error setting run mode (WebSocketException: %s)", e.getMessage()));
593         }
594     }
595
596     public void setSmartWatering(String deviceId, boolean enable) {
597         OrbitBhyveDevice device = getDevice(deviceId);
598         if (device != null && device.getId().equals(deviceId)) {
599             device.setWaterSenseMode(enable ? "auto" : "off");
600             updateDevice(deviceId, gson.toJson(device));
601         }
602     }
603
604     private void updateDevice(String deviceId, String deviceString) {
605         String payload = "{\"device\":" + deviceString + "}";
606         logger.trace("New String: {}", payload);
607         try {
608             ContentResponse response = sendRequestBuilder(BHYVE_DEVICES + "/" + deviceId, HttpMethod.PUT)
609                     .content(new StringContentProvider(payload), "application/json;charset=UTF-8").send();
610             if (logger.isTraceEnabled()) {
611                 logger.trace("Device update response: {}", response.getContentAsString());
612             }
613             if (response.getStatus() != 200) {
614                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
615                         "Update device response status: " + response.getStatus());
616             }
617         } catch (TimeoutException | ExecutionException e) {
618             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating device");
619         } catch (InterruptedException e) {
620             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating device");
621             Thread.currentThread().interrupt();
622         }
623     }
624 }