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