2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.orbitbhyve.internal.handler;
15 import static org.openhab.binding.orbitbhyve.internal.OrbitBhyveBindingConstants.*;
17 import java.io.IOException;
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;
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;
64 import com.google.gson.Gson;
67 * The {@link OrbitBhyveBridgeHandler} is responsible for handling commands, which are
68 * sent to one of the channels.
70 * @author Ondrej Pecta - Initial contribution
73 public class OrbitBhyveBridgeHandler extends ConfigStatusBridgeHandler {
75 private final Logger logger = LoggerFactory.getLogger(OrbitBhyveBridgeHandler.class);
77 private final HttpClient httpClient;
79 private final WebSocketClient webSocketClient;
81 private @Nullable ScheduledFuture<?> future = null;
83 private @Nullable Session session;
85 private @Nullable String sessionToken = null;
87 private OrbitBhyveConfiguration config = new OrbitBhyveConfiguration();
89 private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
92 private final Gson gson = new Gson();
94 public OrbitBhyveBridgeHandler(Bridge thing, HttpClient httpClient, WebSocketClient webSocketClient) {
96 this.httpClient = httpClient;
97 this.webSocketClient = webSocketClient;
101 public Collection<ConfigStatusMessage> getConfigStatus() {
102 return Collections.emptyList();
106 public void handleCommand(ChannelUID channelUID, Command command) {
110 public Collection<Class<? extends ThingHandlerService>> getServices() {
111 return Collections.singleton(OrbitBhyveDiscoveryService.class);
115 public void initialize() {
116 config = getConfigAs(OrbitBhyveConfiguration.class);
117 httpClient.setFollowRedirects(false);
119 scheduler.execute(() -> {
121 future = scheduler.scheduleWithFixedDelay(this::ping, 0, config.refresh, TimeUnit.SECONDS);
123 logger.debug("Finished initializing!");
127 public void dispose() {
128 ScheduledFuture<?> localFuture = future;
129 if (localFuture != null) {
130 localFuture.cancel(true);
136 private boolean login() {
138 String urlParameters = "{\"session\":{\"email\":\"" + config.email + "\",\"password\":\"" + config.password
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());
147 OrbitBhyveSessionResponse session = gson.fromJson(response.getContentAsString(),
148 OrbitBhyveSessionResponse.class);
149 sessionToken = session.getOrbitSessionToken();
150 logger.debug("token: {}", sessionToken);
151 initializeWebSocketSession();
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
154 "Login response status:" + response.getStatus());
157 } catch (TimeoutException | ExecutionException e) {
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
160 } catch (InterruptedException e) {
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
162 Thread.currentThread().interrupt();
165 updateStatus(ThingStatus.ONLINE);
169 private synchronized void ping() {
170 if (ThingStatus.OFFLINE == thing.getStatus()) {
174 if (ThingStatus.ONLINE == thing.getStatus()) {
175 Session localSession = session;
176 if (localSession == null || !localSession.isOpen()) {
177 initializeWebSocketSession();
179 localSession = session;
180 if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
182 logger.debug("Sending ping");
183 localSession.getRemote().sendString("{\"event\":\"ping\"}");
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()));
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Web socket creation error");
198 public List<OrbitBhyveDevice> getDevices() {
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());
205 OrbitBhyveDevice[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveDevice[].class);
206 return Arrays.asList(devices);
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
209 "Get devices returned response status: " + response.getStatus());
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();
217 return new ArrayList<>();
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);
225 public @Nullable OrbitBhyveDevice getDevice(String deviceId) {
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());
232 OrbitBhyveDevice device = gson.fromJson(response.getContentAsString(), OrbitBhyveDevice.class);
235 logger.debug("Returned status: {}", response.getStatus());
236 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
237 "Returned status: " + response.getStatus());
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();
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);
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);
276 case "watering_complete":
277 logger.debug("Watering complete");
278 disableZones(event.getDeviceId());
279 disablePrograms(event.getDeviceId());
280 updateDeviceStatus(event.getDeviceId());
283 logger.debug("Updating mode to: {}", event.getMode());
284 Channel ch = getThingChannel(event.getDeviceId(), CHANNEL_MODE);
286 updateState(ch.getUID(), new StringType(event.getMode()));
288 ch = getThingChannel(event.getDeviceId(), CHANNEL_CONTROL);
290 updateState(ch.getUID(), "off".equals(event.getMode()) ? OnOffType.OFF : OnOffType.ON);
292 updateDeviceStatus(event.getDeviceId());
295 updateDeviceStatus(event.getDeviceId());
297 case "skip_active_station":
298 disableZones(event.getDeviceId());
300 case "program_changed":
301 OrbitBhyveProgram program = gson.fromJson(event.getProgram(), OrbitBhyveProgram.class);
302 if (program != null) {
303 updateDeviceProgramStatus(program);
304 updateDeviceStatus(program.getDeviceId());
308 logger.debug("Received event: {}", event.getEvent());
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);
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());
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);
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);
357 private void disableZones(String deviceId) {
358 disableChannel(deviceId, "zone_");
361 private void disablePrograms(String deviceId) {
362 disableChannel(deviceId, "program_");
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);
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);
384 logger.debug("Cannot find zone: {} for device: {}", station, deviceId);
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);
394 logger.debug("Cannot find channel: {} for device: {}", name, deviceId);
398 private @Nullable Session createSession() {
399 String url = BHYVE_WS_URL;
400 URI uri = URI.create(url);
403 // The socket that receives events
404 OrbitBhyveSocket socket = new OrbitBhyveSocket(this);
406 Future<Session> fut = webSocketClient.connect(socket, uri);
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");
420 private synchronized void initializeWebSocketSession() {
421 logger.debug("Initializing WebSocket session");
423 session = createSession();
424 Session localSession = session;
425 if (localSession != null) {
426 logger.debug("WebSocket connected!");
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()));
441 private void closeSession() {
442 Session localSession = session;
443 if (localSession != null && localSession.isOpen()) {
444 localSession.close();
448 public void runZone(String deviceId, String zone, int time) {
449 String dateTime = format.format(new Date());
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 + "}]}");
459 } catch (IOException e) {
460 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
461 "Error during zone watering execution");
465 public void runProgram(String deviceId, String program) {
466 String dateTime = format.format(new Date());
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 + "\"}");
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()));
483 public void enableProgram(OrbitBhyveProgram program, boolean enable) {
485 String payLoad = "{\"sprinkler_timer_program\":{\"id\":\"" + program.getId() + "\",\"device_id\":\""
486 + program.getDeviceId() + "\",\"program\":\"" + program.getProgram() + "\",\"enabled\":" + enable
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());
497 logger.debug("Returned status: {}", response.getStatus());
498 updateStatus(ThingStatus.OFFLINE);
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();
508 public void setRainDelay(String deviceId, int delay) {
509 String dateTime = format.format(new Date());
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 + "\"}");
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()));
526 public void stopWatering(String deviceId) {
527 String dateTime = format.format(new Date());
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\":[]}");
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()));
544 public List<OrbitBhyveProgram> getPrograms() {
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());
551 OrbitBhyveProgram[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveProgram[].class);
552 return Arrays.asList(devices);
554 logger.debug("Returned status: {}", response.getStatus());
555 updateStatus(ThingStatus.OFFLINE);
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();
563 return new ArrayList<>();
566 public void changeRunMode(String deviceId, String mode) {
567 String dateTime = format.format(new Date());
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 + "\"}");
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()));
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));
592 private void updateDevice(String deviceId, String deviceString) {
593 String payload = "{\"device\":" + deviceString + "}";
594 logger.trace("New String: {}", payload);
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());
601 if (response.getStatus() != 200) {
602 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
603 "Update device response status: " + response.getStatus());
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();