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.client.WebSocketClient;
41 import org.openhab.binding.orbitbhyve.internal.OrbitBhyveConfiguration;
42 import org.openhab.binding.orbitbhyve.internal.discovery.OrbitBhyveDiscoveryService;
43 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveDevice;
44 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveProgram;
45 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveSessionResponse;
46 import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveSocketEvent;
47 import org.openhab.binding.orbitbhyve.internal.net.OrbitBhyveSocket;
48 import org.openhab.core.config.core.status.ConfigStatusMessage;
49 import org.openhab.core.library.types.OnOffType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.thing.Bridge;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
58 import org.openhab.core.thing.binding.ThingHandlerService;
59 import org.openhab.core.types.Command;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
63 import com.google.gson.Gson;
66 * The {@link OrbitBhyveBridgeHandler} is responsible for handling commands, which are
67 * sent to one of the channels.
69 * @author Ondrej Pecta - Initial contribution
72 public class OrbitBhyveBridgeHandler extends ConfigStatusBridgeHandler {
74 private final Logger logger = LoggerFactory.getLogger(OrbitBhyveBridgeHandler.class);
76 private final HttpClient httpClient;
78 private final WebSocketClient webSocketClient;
80 private @Nullable ScheduledFuture<?> future = null;
82 private @Nullable Session session;
84 private @Nullable String sessionToken = null;
86 private OrbitBhyveConfiguration config = new OrbitBhyveConfiguration();
88 private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
91 private final Gson gson = new Gson();
93 public OrbitBhyveBridgeHandler(Bridge thing, HttpClient httpClient, WebSocketClient webSocketClient) {
95 this.httpClient = httpClient;
96 this.webSocketClient = webSocketClient;
100 public Collection<ConfigStatusMessage> getConfigStatus() {
101 return Collections.emptyList();
105 public void handleCommand(ChannelUID channelUID, Command command) {
109 public Collection<Class<? extends ThingHandlerService>> getServices() {
110 return Collections.singleton(OrbitBhyveDiscoveryService.class);
114 public void initialize() {
115 config = getConfigAs(OrbitBhyveConfiguration.class);
116 httpClient.setFollowRedirects(false);
118 scheduler.execute(() -> {
120 future = scheduler.scheduleWithFixedDelay(this::ping, 0, config.refresh, TimeUnit.SECONDS);
122 logger.debug("Finished initializing!");
126 public void dispose() {
127 ScheduledFuture<?> localFuture = future;
128 if (localFuture != null) {
129 localFuture.cancel(true);
135 private boolean login() {
137 String urlParameters = "{\"session\":{\"email\":\"" + config.email + "\",\"password\":\"" + config.password
139 ContentResponse response = httpClient.newRequest(BHYVE_SESSION).method(HttpMethod.POST).agent(AGENT)
140 .content(new StringContentProvider(urlParameters), "application/json; charset=utf-8")
141 .timeout(BHYVE_TIMEOUT, TimeUnit.SECONDS).send();
142 if (response.getStatus() == 200) {
143 if (logger.isTraceEnabled()) {
144 logger.trace("response: {}", response.getContentAsString());
146 OrbitBhyveSessionResponse session = gson.fromJson(response.getContentAsString(),
147 OrbitBhyveSessionResponse.class);
148 sessionToken = session.getOrbitSessionToken();
149 logger.debug("token: {}", sessionToken);
150 initializeWebSocketSession();
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
153 "Login response status:" + response.getStatus());
156 } catch (TimeoutException | ExecutionException e) {
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
159 } catch (InterruptedException e) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
161 Thread.currentThread().interrupt();
164 updateStatus(ThingStatus.ONLINE);
168 private synchronized void ping() {
169 if (ThingStatus.OFFLINE == thing.getStatus()) {
173 if (ThingStatus.ONLINE == thing.getStatus()) {
174 Session localSession = session;
175 if (localSession == null || !localSession.isOpen()) {
176 initializeWebSocketSession();
178 localSession = session;
179 if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
181 logger.debug("Sending ping");
182 localSession.getRemote().sendString("{\"event\":\"ping\"}");
184 } catch (IOException e) {
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
186 "Error sending ping to a web socket");
189 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Web socket creation error");
194 public List<OrbitBhyveDevice> getDevices() {
196 ContentResponse response = sendRequestBuilder(BHYVE_DEVICES, HttpMethod.GET).send();
197 if (response.getStatus() == 200) {
198 if (logger.isTraceEnabled()) {
199 logger.trace("Devices response: {}", response.getContentAsString());
201 OrbitBhyveDevice[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveDevice[].class);
202 return Arrays.asList(devices);
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
205 "Get devices returned response status: " + response.getStatus());
207 } catch (TimeoutException | ExecutionException e) {
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting devices");
209 } catch (InterruptedException e) {
210 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting devices");
211 Thread.currentThread().interrupt();
213 return new ArrayList<>();
216 Request sendRequestBuilder(String uri, HttpMethod method) {
217 return httpClient.newRequest(uri).method(method).agent(AGENT).header("Orbit-Session-Token", sessionToken)
218 .timeout(BHYVE_TIMEOUT, TimeUnit.SECONDS);
221 public @Nullable OrbitBhyveDevice getDevice(String deviceId) {
223 ContentResponse response = sendRequestBuilder(BHYVE_DEVICES + "/" + deviceId, HttpMethod.GET).send();
224 if (response.getStatus() == 200) {
225 if (logger.isTraceEnabled()) {
226 logger.trace("Device response: {}", response.getContentAsString());
228 OrbitBhyveDevice device = gson.fromJson(response.getContentAsString(), OrbitBhyveDevice.class);
231 logger.debug("Returned status: {}", response.getStatus());
232 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
233 "Returned status: " + response.getStatus());
235 } catch (TimeoutException | ExecutionException e) {
236 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
237 "Error during getting device info: " + deviceId);
238 } catch (InterruptedException e) {
239 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
240 "Error during getting device info: " + deviceId);
241 Thread.currentThread().interrupt();
246 public synchronized void processStatusResponse(String content) {
247 updateStatus(ThingStatus.ONLINE);
248 logger.trace("Got message: {}", content);
249 OrbitBhyveSocketEvent event = gson.fromJson(content, OrbitBhyveSocketEvent.class);
255 private void processEvent(OrbitBhyveSocketEvent event) {
256 switch (event.getEvent()) {
257 case "watering_in_progress_notification":
258 disableZones(event.getDeviceId());
259 Channel channel = getThingChannel(event.getDeviceId(), event.getStation());
260 if (channel != null) {
261 logger.debug("Watering zone: {}", event.getStation());
262 updateState(channel.getUID(), OnOffType.ON);
263 String program = event.getProgram().getAsString();
264 if (!program.isEmpty() && !"manual".equals(program)) {
265 channel = getThingChannel(event.getDeviceId(), "program_" + program);
266 if (channel != null) {
267 updateState(channel.getUID(), OnOffType.ON);
272 case "watering_complete":
273 logger.debug("Watering complete");
274 disableZones(event.getDeviceId());
275 disablePrograms(event.getDeviceId());
276 updateDeviceStatus(event.getDeviceId());
279 logger.debug("Updating mode to: {}", event.getMode());
280 Channel ch = getThingChannel(event.getDeviceId(), CHANNEL_MODE);
282 updateState(ch.getUID(), new StringType(event.getMode()));
284 ch = getThingChannel(event.getDeviceId(), CHANNEL_CONTROL);
286 updateState(ch.getUID(), "off".equals(event.getMode()) ? OnOffType.OFF : OnOffType.ON);
288 updateDeviceStatus(event.getDeviceId());
291 updateDeviceStatus(event.getDeviceId());
293 case "skip_active_station":
294 disableZones(event.getDeviceId());
296 case "program_changed":
297 OrbitBhyveProgram program = gson.fromJson(event.getProgram(), OrbitBhyveProgram.class);
298 if (program != null) {
299 updateDeviceProgramStatus(program);
300 updateDeviceStatus(program.getDeviceId());
304 logger.debug("Received event: {}", event.getEvent());
308 private void updateAllStatuses() {
309 List<OrbitBhyveDevice> devices = getDevices();
310 for (Thing th : getThing().getThings()) {
311 if (th.isEnabled()) {
312 String deviceId = th.getUID().getId();
313 OrbitBhyveSprinklerHandler handler = (OrbitBhyveSprinklerHandler) th.getHandler();
314 for (OrbitBhyveDevice device : devices) {
315 if (deviceId.equals(th.getUID().getId())) {
316 updateDeviceStatus(device, handler);
323 private void updateDeviceStatus(@Nullable OrbitBhyveDevice device, @Nullable OrbitBhyveSprinklerHandler handler) {
324 if (device != null && handler != null) {
325 handler.setDeviceOnline(device.isConnected());
326 handler.updateDeviceStatus(device.getStatus());
327 handler.updateSmartWatering(device.getWaterSenseMode());
332 private void updateDeviceStatus(String deviceId) {
333 for (Thing th : getThing().getThings()) {
334 if (deviceId.equals(th.getUID().getId())) {
335 OrbitBhyveSprinklerHandler handler = (OrbitBhyveSprinklerHandler) th.getHandler();
336 OrbitBhyveDevice device = getDevice(deviceId);
337 updateDeviceStatus(device, handler);
342 private void updateDeviceProgramStatus(OrbitBhyveProgram program) {
343 for (Thing th : getThing().getThings()) {
344 if (program.getDeviceId().equals(th.getUID().getId())) {
345 OrbitBhyveSprinklerHandler handler = (OrbitBhyveSprinklerHandler) th.getHandler();
346 if (handler != null) {
347 handler.updateProgram(program);
353 private void disableZones(String deviceId) {
354 disableChannel(deviceId, "zone_");
357 private void disablePrograms(String deviceId) {
358 disableChannel(deviceId, "program_");
361 private void disableChannel(String deviceId, String name) {
362 for (Thing th : getThing().getThings()) {
363 if (deviceId.equals(th.getUID().getId())) {
364 for (Channel ch : th.getChannels()) {
365 if (ch.getUID().getId().startsWith(name)) {
366 updateState(ch.getUID(), OnOffType.OFF);
374 private @Nullable Channel getThingChannel(String deviceId, int station) {
375 for (Thing th : getThing().getThings()) {
376 if (deviceId.equals(th.getUID().getId())) {
377 return th.getChannel("zone_" + station);
380 logger.debug("Cannot find zone: {} for device: {}", station, deviceId);
384 private @Nullable Channel getThingChannel(String deviceId, String name) {
385 for (Thing th : getThing().getThings()) {
386 if (deviceId.equals(th.getUID().getId())) {
387 return th.getChannel(name);
390 logger.debug("Cannot find channel: {} for device: {}", name, deviceId);
394 private @Nullable Session createSession() {
395 String url = BHYVE_WS_URL;
396 URI uri = URI.create(url);
399 // The socket that receives events
400 OrbitBhyveSocket socket = new OrbitBhyveSocket(this);
402 Future<Session> fut = webSocketClient.connect(socket, uri);
405 } catch (IOException e) {
406 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot connect websocket client");
407 } catch (InterruptedException e) {
408 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
409 Thread.currentThread().interrupt();
410 } catch (ExecutionException e) {
411 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
416 private synchronized void initializeWebSocketSession() {
417 logger.debug("Initializing WebSocket session");
419 session = createSession();
420 Session localSession = session;
421 if (localSession != null) {
422 logger.debug("WebSocket connected!");
424 String msg = "{\"event\":\"app_connection\",\"orbit_session_token\":\"" + sessionToken + "\"}";
425 logger.trace("sending message:\n {}", msg);
426 localSession.getRemote().sendString(msg);
427 } catch (IOException e) {
428 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
429 "Cannot send hello string to web socket!");
434 private void closeSession() {
435 Session localSession = session;
436 if (localSession != null && localSession.isOpen()) {
437 localSession.close();
441 public void runZone(String deviceId, String zone, int time) {
442 String dateTime = format.format(new Date());
445 Session localSession = session;
446 if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
447 localSession.getRemote()
448 .sendString("{\"event\":\"change_mode\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\""
449 + dateTime + "\",\"mode\":\"manual\",\"stations\":[{\"station\":" + zone
450 + ",\"run_time\":" + time + "}]}");
452 } catch (IOException e) {
453 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
454 "Error during zone watering execution");
458 public void runProgram(String deviceId, String program) {
459 String dateTime = format.format(new Date());
462 Session localSession = session;
463 if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
464 localSession.getRemote().sendString("{\"event\":\"change_mode\",\"mode\":\"manual\",\"program\":\""
465 + program + "\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\"" + dateTime + "\"}");
467 } catch (IOException e) {
468 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
469 "Error during program watering execution");
473 public void enableProgram(OrbitBhyveProgram program, boolean enable) {
475 String payLoad = "{\"sprinkler_timer_program\":{\"id\":\"" + program.getId() + "\",\"device_id\":\""
476 + program.getDeviceId() + "\",\"program\":\"" + program.getProgram() + "\",\"enabled\":" + enable
478 logger.debug("updating program {} with data {}", program.getProgram(), payLoad);
479 ContentResponse response = sendRequestBuilder(BHYVE_PROGRAMS + "/" + program.getId(), HttpMethod.PUT)
480 .content(new StringContentProvider(payLoad), "application/json; charset=utf-8").send();
481 if (response.getStatus() == 200) {
482 if (logger.isTraceEnabled()) {
483 logger.trace("Enable programs response: {}", response.getContentAsString());
487 logger.debug("Returned status: {}", response.getStatus());
488 updateStatus(ThingStatus.OFFLINE);
490 } catch (TimeoutException | ExecutionException e) {
491 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating programs");
492 } catch (InterruptedException e) {
493 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating programs");
494 Thread.currentThread().interrupt();
498 public void setRainDelay(String deviceId, int delay) {
499 String dateTime = format.format(new Date());
502 Session localSession = session;
503 if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
504 localSession.getRemote().sendString("{\"event\":\"rain_delay\",\"device_id\":\"" + deviceId
505 + "\",\"delay\":" + delay + ",\"timestamp\":\"" + dateTime + "\"}");
507 } catch (IOException e) {
508 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during rain delay setting");
512 public void stopWatering(String deviceId) {
513 String dateTime = format.format(new Date());
516 Session localSession = session;
517 if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
518 localSession.getRemote().sendString("{\"event\":\"change_mode\",\"device_id\":\"" + deviceId
519 + "\",\"timestamp\":\"" + dateTime + "\",\"mode\":\"manual\",\"stations\":[]}");
521 } catch (IOException e) {
522 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during watering stopping");
526 public List<OrbitBhyveProgram> getPrograms() {
528 ContentResponse response = sendRequestBuilder(BHYVE_PROGRAMS, HttpMethod.GET).send();
529 if (response.getStatus() == 200) {
530 if (logger.isTraceEnabled()) {
531 logger.trace("Programs response: {}", response.getContentAsString());
533 OrbitBhyveProgram[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveProgram[].class);
534 return Arrays.asList(devices);
536 logger.debug("Returned status: {}", response.getStatus());
537 updateStatus(ThingStatus.OFFLINE);
539 } catch (TimeoutException | ExecutionException e) {
540 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs");
541 } catch (InterruptedException e) {
542 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs");
543 Thread.currentThread().interrupt();
545 return new ArrayList<>();
548 public void changeRunMode(String deviceId, String mode) {
549 String dateTime = format.format(new Date());
552 Session localSession = session;
553 if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
554 localSession.getRemote().sendString("{\"event\":\"change_mode\",\"mode\":\"" + mode
555 + "\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\"" + dateTime + "\"}");
557 } catch (IOException e) {
558 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during setting run mode");
562 public void setSmartWatering(String deviceId, boolean enable) {
563 OrbitBhyveDevice device = getDevice(deviceId);
564 if (device != null && device.getId().equals(deviceId)) {
565 device.setWaterSenseMode(enable ? "auto" : "off");
566 updateDevice(deviceId, gson.toJson(device));
570 private void updateDevice(String deviceId, String deviceString) {
571 String payload = "{\"device\":" + deviceString + "}";
572 logger.trace("New String: {}", payload);
574 ContentResponse response = sendRequestBuilder(BHYVE_DEVICES + "/" + deviceId, HttpMethod.PUT)
575 .content(new StringContentProvider(payload), "application/json;charset=UTF-8").send();
576 if (logger.isTraceEnabled()) {
577 logger.trace("Device update response: {}", response.getContentAsString());
579 if (response.getStatus() != 200) {
580 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
581 "Update device response status: " + response.getStatus());
583 } catch (TimeoutException | ExecutionException e) {
584 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating device");
585 } catch (InterruptedException e) {
586 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating device");
587 Thread.currentThread().interrupt();