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