diff --git a/CODEOWNERS b/CODEOWNERS
index bd8485467bc..a78c5cbb04e 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -222,6 +222,7 @@
/bundles/org.openhab.binding.openweathermap/ @cweitkamp
/bundles/org.openhab.binding.openwebnet/ @mvalla
/bundles/org.openhab.binding.oppo/ @mlobstein
+/bundles/org.openhab.binding.orbitbhyve/ @octa22
/bundles/org.openhab.binding.orvibo/ @tavalin
/bundles/org.openhab.binding.paradoxalarm/ @theater
/bundles/org.openhab.binding.pentair/ @jsjames
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 7830bf7d35c..f4184174b7c 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1091,6 +1091,11 @@
org.openhab.binding.oppo
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.orbitbhyve
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.orvibo
diff --git a/bundles/org.openhab.binding.orbitbhyve/NOTICE b/bundles/org.openhab.binding.orbitbhyve/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.orbitbhyve/README.md b/bundles/org.openhab.binding.orbitbhyve/README.md
new file mode 100644
index 00000000000..9d0d8874070
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/README.md
@@ -0,0 +1,84 @@
+# Orbit B-hyve Binding
+
+This is the binding for the [Orbit B-hyve](https://bhyve.orbitonline.com/) wi-fi sprinklers.
+
+## Supported Things
+
+This binding should support all the sprinklers which can be controlled by the Orbit B-hyve mobile application.
+So far only the [Orbit B-hyve 8-zone Indoor Timer](https://bhyve.orbitonline.com/indoor-timer/) has been confirmed working. (Hardware version WT24-0001)
+
+## Discovery
+
+This binding supports the auto discovery of the sprinklers bound to your Orbit B-hyve account.
+To start the discovery you need to create a bridge thing and enter valid credentials to your Orbit B-hyve cloud account.
+
+## Thing Configuration
+
+The bridge thing requires a manual configuration. You have to enter valid credentials to your Orbit B-hyve account, and you can also set the refresh time in seconds for polling data from the Orbit cloud.
+There is no user configuration related to sprinkler things. Sprinklers do need a configuration property _id_ identifying the device, but the only way how to retrieve it is to let the bridge to auto discover sprinklers.
+
+## Channels
+
+This binding automatically detects all zones and programs for each sprinkler and creates these dynamic channels:
+
+| channel | type | description |
+|------------------|--------|------------------------------------------------------------------|
+| zone_% | Switch | This channel controls the manual zone watering (ON/OFF) |
+| program_% | Switch | This channel controls the manual program watering (ON/OFF) |
+| enable_program_% | Switch | This channel controls the automatic program scheduling (ON/OFF) |
+
+Beside the dynamic channels each sprinkler thing provides these standard channels:
+
+| channel | type | description |
+|----------------|-------------|--------------------------------------------------------------------|
+| mode | String | This channel represents the mode of sprinkler device (auto/manual) |
+| next_start | DateTime | This channel represents the start time of the next watering |
+| rain_delay | Number:Time | This channel manages the current rain delay in hours |
+| watering_time | Number:Time | This channel manages the manual zone watering time in minutes |
+| control | Switch | This channel controls the sprinkler (ON/OFF) |
+| smart_watering | Switch | This channel controls the smart watering (ON/OFF) |
+
+## Full Example
+
+_*.things example_
+
+```
+Bridge orbitbhyve:bridge:mybridge "Orbit Bridge" [ email="your@ema.il", password="yourPass", refresh=30 ] {
+ Thing sprinkler indoor_timer "Sprinkler" [ id="4cab55704e0d7ddf98c1cc37" ]
+}
+```
+
+_*.items example_
+
+```
+Switch IrrigationControl "Irrigation active" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:control" }
+Switch IrrigationSmartWatering "Smart watering" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:smart_watering" }
+Switch Irrigation1 "Zone 1" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_1" }
+Switch Irrigation2 "Zone 2" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_2" }
+Switch Irrigation3 "Zone 3" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_3" }
+Switch Irrigation4 "Zone 4" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_4" }
+Switch IrrigationP1 "Run program A" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:program_a" }
+Switch IrrigationP1Enable "Schedule program A" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:enable_program_a" }
+String IrrigationMode "Irrigation mode [%s]" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:mode" }
+Number IrrigationTime "Irrigation time [%d min]" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:watering_time" }
+Number IrrigationRainDelay "Rain delay [%d h]" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:rain_delay" }
+DateTime IrrigationNextStart "Next start A [%1$td.%1$tm.%1$tY %1$tR]" (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:next_start" }
+```
+
+_*.sitemap example_
+
+```
+Switch item=IrrigationControl
+Switch item=IrrigationSmartWatering
+Switch item=Irrigation1
+Switch item=Irrigation2
+Switch item=Irrigation3
+Switch item=Irrigation4
+Setpoint item=IrrigationTime minValue=1 maxValue=240 step=1
+Switch item=IrrigationP1
+Switch item=IrrigationP1Enable
+Text item=IrrigationMode
+Text item=IrrigationRainDelay
+Switch item=IrrigationRainDelay mappings=[0="OFF", 24="24", 48="48", 72="72"]
+Text item=IrrigationNextStart visibility=[IrrigationP1Enable==ON]
+```
diff --git a/bundles/org.openhab.binding.orbitbhyve/pom.xml b/bundles/org.openhab.binding.orbitbhyve/pom.xml
new file mode 100644
index 00000000000..332ae978100
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.2.0-SNAPSHOT
+
+
+ org.openhab.binding.orbitbhyve
+
+ openHAB Add-ons :: Bundles :: Orbit B-hyve Binding
+
+
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/feature/feature.xml b/bundles/org.openhab.binding.orbitbhyve/src/main/feature/feature.xml
new file mode 100644
index 00000000000..23c479ac0f0
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.orbitbhyve/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveBindingConstants.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveBindingConstants.java
new file mode 100644
index 00000000000..888659641b1
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveBindingConstants.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link OrbitBhyveBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveBindingConstants {
+
+ public static final String BINDING_ID = "orbitbhyve";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
+ public static final ThingTypeUID THING_TYPE_SPRINKLER = new ThingTypeUID(BINDING_ID, "sprinkler");
+
+ // List of all Channel ids
+ public static final String CHANNEL_CONTROL = "control";
+ public static final String CHANNEL_MODE = "mode";
+ public static final String CHANNEL_SMART_WATERING = "smart_watering";
+ public static final String CHANNEL_NEXT_START = "next_start";
+ public static final String CHANNEL_RAIN_DELAY = "rain_delay";
+ public static final String CHANNEL_WATERING_TIME = "watering_time";
+
+ // Constants
+ public static final String AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36";
+ public static final String BHYVE_API = "https://api.orbitbhyve.com/v1/";
+ public static final String BHYVE_SESSION = BHYVE_API + "session";
+ public static final String BHYVE_DEVICES = BHYVE_API + "devices";
+ public static final String BHYVE_PROGRAMS = BHYVE_API + "sprinkler_timer_programs";
+ public static final String BHYVE_WS_URL = "wss://api.orbitbhyve.com/v1/events";
+ public static final int BHYVE_TIMEOUT = 5;
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveConfiguration.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveConfiguration.java
new file mode 100644
index 00000000000..24a6de6356d
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveConfiguration.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link OrbitBhyveConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveConfiguration {
+
+ /**
+ * Sample configuration parameter. Replace with your own.
+ */
+ public String email = "";
+ public String password = "";
+ public int refresh = 30;
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveHandlerFactory.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveHandlerFactory.java
new file mode 100644
index 00000000000..c392d5f189b
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveHandlerFactory.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal;
+
+import static org.openhab.binding.orbitbhyve.internal.OrbitBhyveBindingConstants.THING_TYPE_BRIDGE;
+import static org.openhab.binding.orbitbhyve.internal.OrbitBhyveBindingConstants.THING_TYPE_SPRINKLER;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.openhab.binding.orbitbhyve.internal.handler.OrbitBhyveBridgeHandler;
+import org.openhab.binding.orbitbhyve.internal.handler.OrbitBhyveSprinklerHandler;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.io.net.http.WebSocketFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link OrbitBhyveHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.orbitbhyve", service = ThingHandlerFactory.class)
+public class OrbitBhyveHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_SPRINKLER);
+
+ /**
+ * the shared http client
+ */
+ private HttpClient httpClient;
+
+ /**
+ * the shared web socket client
+ */
+ private WebSocketClient webSocketClient;
+
+ @Activate
+ public OrbitBhyveHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+ @Reference WebSocketFactory webSocketFactory) {
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.webSocketClient = webSocketFactory.getCommonWebSocketClient();
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
+ return new OrbitBhyveBridgeHandler((Bridge) thing, httpClient, webSocketClient);
+ }
+ if (THING_TYPE_SPRINKLER.equals(thingTypeUID)) {
+ return new OrbitBhyveSprinklerHandler(thing);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/discovery/OrbitBhyveDiscoveryService.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/discovery/OrbitBhyveDiscoveryService.java
new file mode 100644
index 00000000000..68974ee2564
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/discovery/OrbitBhyveDiscoveryService.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.discovery;
+
+import static org.openhab.binding.orbitbhyve.internal.OrbitBhyveBindingConstants.THING_TYPE_SPRINKLER;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.orbitbhyve.internal.handler.OrbitBhyveBridgeHandler;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveDevice;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link OrbitBhyveDiscoveryService} discovers sprinklers
+ * associated with your Orbit B-Hyve cloud account.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveDiscoveryService extends AbstractDiscoveryService
+ implements DiscoveryService, ThingHandlerService {
+
+ private final Logger logger = LoggerFactory.getLogger(OrbitBhyveDiscoveryService.class);
+
+ private @Nullable OrbitBhyveBridgeHandler bridgeHandler;
+
+ private @Nullable ScheduledFuture> discoveryJob;
+
+ private static final int DISCOVERY_TIMEOUT_SEC = 10;
+ private static final int DISCOVERY_REFRESH_SEC = 1800;
+
+ public OrbitBhyveDiscoveryService() {
+ super(DISCOVERY_TIMEOUT_SEC);
+ logger.debug("Creating discovery service");
+ }
+
+ @Override
+ protected void startScan() {
+ runDiscovery();
+ }
+
+ @Override
+ public void activate() {
+ super.activate(null);
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler thingHandler) {
+ if (thingHandler instanceof OrbitBhyveBridgeHandler) {
+ bridgeHandler = (OrbitBhyveBridgeHandler) thingHandler;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler;
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ logger.debug("Starting Orbit B-Hyve background discovery");
+
+ ScheduledFuture> localDiscoveryJob = discoveryJob;
+ if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
+ discoveryJob = scheduler.scheduleWithFixedDelay(this::runDiscovery, 10, DISCOVERY_REFRESH_SEC,
+ TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ logger.debug("Stopping Orbit B-Hyve background discovery");
+ ScheduledFuture> localDiscoveryJob = discoveryJob;
+ if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
+ localDiscoveryJob.cancel(true);
+ }
+ }
+
+ private synchronized void runDiscovery() {
+ OrbitBhyveBridgeHandler localBridgeHandler = bridgeHandler;
+ if (localBridgeHandler != null && ThingStatus.ONLINE == localBridgeHandler.getThing().getStatus()) {
+ List devices = localBridgeHandler.getDevices();
+ logger.debug("Discovered total of {} devices", devices.size());
+ for (OrbitBhyveDevice device : devices) {
+ sprinklerDiscovered(device);
+ }
+ }
+ }
+
+ private void sprinklerDiscovered(OrbitBhyveDevice device) {
+ OrbitBhyveBridgeHandler localBridgeHandler = bridgeHandler;
+ if (localBridgeHandler != null) {
+ Map properties = new HashMap<>();
+ properties.put("id", device.getId());
+ properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFwVersion());
+ properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.getHwVersion());
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getMacAddress());
+ properties.put(Thing.PROPERTY_MODEL_ID, device.getType());
+ properties.put("Zones", device.getNumStations());
+ properties.put("Active zones", device.getZones().size());
+
+ ThingUID thingUID = new ThingUID(THING_TYPE_SPRINKLER, localBridgeHandler.getThing().getUID(),
+ device.getId());
+
+ logger.debug("Detected a/an {} - label: {} id: {}", THING_TYPE_SPRINKLER.getId(), device.getName(),
+ device.getId());
+ thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_SPRINKLER)
+ .withProperties(properties).withRepresentationProperty("id").withLabel(device.getName())
+ .withBridge(localBridgeHandler.getThing().getUID()).build());
+ }
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return Collections.singleton(THING_TYPE_SPRINKLER);
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveBridgeHandler.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveBridgeHandler.java
new file mode 100644
index 00000000000..73e66851364
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveBridgeHandler.java
@@ -0,0 +1,588 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.handler;
+
+import static org.openhab.binding.orbitbhyve.internal.OrbitBhyveBindingConstants.*;
+
+import java.io.IOException;
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.openhab.binding.orbitbhyve.internal.OrbitBhyveConfiguration;
+import org.openhab.binding.orbitbhyve.internal.discovery.OrbitBhyveDiscoveryService;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveDevice;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveProgram;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveSessionResponse;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveSocketEvent;
+import org.openhab.binding.orbitbhyve.internal.net.OrbitBhyveSocket;
+import org.openhab.core.config.core.status.ConfigStatusMessage;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link OrbitBhyveBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveBridgeHandler extends ConfigStatusBridgeHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(OrbitBhyveBridgeHandler.class);
+
+ private final HttpClient httpClient;
+
+ private final WebSocketClient webSocketClient;
+
+ private @Nullable ScheduledFuture> future = null;
+
+ private @Nullable Session session;
+
+ private @Nullable String sessionToken = null;
+
+ private OrbitBhyveConfiguration config = new OrbitBhyveConfiguration();
+
+ private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+
+ // Gson & parser
+ private final Gson gson = new Gson();
+
+ public OrbitBhyveBridgeHandler(Bridge thing, HttpClient httpClient, WebSocketClient webSocketClient) {
+ super(thing);
+ this.httpClient = httpClient;
+ this.webSocketClient = webSocketClient;
+ }
+
+ @Override
+ public Collection getConfigStatus() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Collections.singleton(OrbitBhyveDiscoveryService.class);
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(OrbitBhyveConfiguration.class);
+ httpClient.setFollowRedirects(false);
+
+ scheduler.execute(() -> {
+ login();
+ future = scheduler.scheduleWithFixedDelay(this::ping, 0, config.refresh, TimeUnit.SECONDS);
+ });
+ logger.debug("Finished initializing!");
+ }
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> localFuture = future;
+ if (localFuture != null) {
+ localFuture.cancel(true);
+ }
+ closeSession();
+ super.dispose();
+ }
+
+ private boolean login() {
+ try {
+ String urlParameters = "{\"session\":{\"email\":\"" + config.email + "\",\"password\":\"" + config.password
+ + "\"}}";
+ ContentResponse response = httpClient.newRequest(BHYVE_SESSION).method(HttpMethod.POST).agent(AGENT)
+ .content(new StringContentProvider(urlParameters), "application/json; charset=utf-8")
+ .timeout(BHYVE_TIMEOUT, TimeUnit.SECONDS).send();
+ if (response.getStatus() == 200) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("response: {}", response.getContentAsString());
+ }
+ OrbitBhyveSessionResponse session = gson.fromJson(response.getContentAsString(),
+ OrbitBhyveSessionResponse.class);
+ sessionToken = session.getOrbitSessionToken();
+ logger.debug("token: {}", sessionToken);
+ initializeWebSocketSession();
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Login response status:" + response.getStatus());
+ return false;
+ }
+ } catch (TimeoutException | ExecutionException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
+ return false;
+ } catch (InterruptedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Exception during login");
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ updateStatus(ThingStatus.ONLINE);
+ return true;
+ }
+
+ private synchronized void ping() {
+ if (ThingStatus.OFFLINE == thing.getStatus()) {
+ login();
+ }
+
+ if (ThingStatus.ONLINE == thing.getStatus()) {
+ Session localSession = session;
+ if (localSession == null || !localSession.isOpen()) {
+ initializeWebSocketSession();
+ }
+ localSession = session;
+ if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
+ try {
+ logger.debug("Sending ping");
+ localSession.getRemote().sendString("{\"event\":\"ping\"}");
+ updateAllStatuses();
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Error sending ping to a web socket");
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Web socket creation error");
+ }
+ }
+ }
+
+ public List getDevices() {
+ try {
+ ContentResponse response = sendRequestBuilder(BHYVE_DEVICES, HttpMethod.GET).send();
+ if (response.getStatus() == 200) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Devices response: {}", response.getContentAsString());
+ }
+ OrbitBhyveDevice[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveDevice[].class);
+ return Arrays.asList(devices);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Get devices returned response status: " + response.getStatus());
+ }
+ } catch (TimeoutException | ExecutionException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting devices");
+ } catch (InterruptedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting devices");
+ Thread.currentThread().interrupt();
+ }
+ return new ArrayList<>();
+ }
+
+ Request sendRequestBuilder(String uri, HttpMethod method) {
+ return httpClient.newRequest(uri).method(method).agent(AGENT).header("Orbit-Session-Token", sessionToken)
+ .timeout(BHYVE_TIMEOUT, TimeUnit.SECONDS);
+ }
+
+ public @Nullable OrbitBhyveDevice getDevice(String deviceId) {
+ try {
+ ContentResponse response = sendRequestBuilder(BHYVE_DEVICES + "/" + deviceId, HttpMethod.GET).send();
+ if (response.getStatus() == 200) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Device response: {}", response.getContentAsString());
+ }
+ OrbitBhyveDevice device = gson.fromJson(response.getContentAsString(), OrbitBhyveDevice.class);
+ return device;
+ } else {
+ logger.debug("Returned status: {}", response.getStatus());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Returned status: " + response.getStatus());
+ }
+ } catch (TimeoutException | ExecutionException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Error during getting device info: " + deviceId);
+ } catch (InterruptedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Error during getting device info: " + deviceId);
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+
+ public synchronized void processStatusResponse(String content) {
+ updateStatus(ThingStatus.ONLINE);
+ logger.trace("Got message: {}", content);
+ OrbitBhyveSocketEvent event = gson.fromJson(content, OrbitBhyveSocketEvent.class);
+ if (event != null) {
+ processEvent(event);
+ }
+ }
+
+ private void processEvent(OrbitBhyveSocketEvent event) {
+ switch (event.getEvent()) {
+ case "watering_in_progress_notification":
+ disableZones(event.getDeviceId());
+ Channel channel = getThingChannel(event.getDeviceId(), event.getStation());
+ if (channel != null) {
+ logger.debug("Watering zone: {}", event.getStation());
+ updateState(channel.getUID(), OnOffType.ON);
+ String program = event.getProgram().getAsString();
+ if (!program.isEmpty() && !"manual".equals(program)) {
+ channel = getThingChannel(event.getDeviceId(), "program_" + program);
+ if (channel != null) {
+ updateState(channel.getUID(), OnOffType.ON);
+ }
+ }
+ }
+ break;
+ case "watering_complete":
+ logger.debug("Watering complete");
+ disableZones(event.getDeviceId());
+ disablePrograms(event.getDeviceId());
+ updateDeviceStatus(event.getDeviceId());
+ break;
+ case "change_mode":
+ logger.debug("Updating mode to: {}", event.getMode());
+ Channel ch = getThingChannel(event.getDeviceId(), CHANNEL_MODE);
+ if (ch != null) {
+ updateState(ch.getUID(), new StringType(event.getMode()));
+ }
+ ch = getThingChannel(event.getDeviceId(), CHANNEL_CONTROL);
+ if (ch != null) {
+ updateState(ch.getUID(), "off".equals(event.getMode()) ? OnOffType.OFF : OnOffType.ON);
+ }
+ updateDeviceStatus(event.getDeviceId());
+ break;
+ case "rain_delay":
+ updateDeviceStatus(event.getDeviceId());
+ break;
+ case "skip_active_station":
+ disableZones(event.getDeviceId());
+ break;
+ case "program_changed":
+ OrbitBhyveProgram program = gson.fromJson(event.getProgram(), OrbitBhyveProgram.class);
+ if (program != null) {
+ updateDeviceProgramStatus(program);
+ updateDeviceStatus(program.getDeviceId());
+ }
+ break;
+ default:
+ logger.debug("Received event: {}", event.getEvent());
+ }
+ }
+
+ private void updateAllStatuses() {
+ List devices = getDevices();
+ for (Thing th : getThing().getThings()) {
+ String deviceId = th.getUID().getId();
+ OrbitBhyveSprinklerHandler handler = (OrbitBhyveSprinklerHandler) th.getHandler();
+ for (OrbitBhyveDevice device : devices) {
+ if (deviceId.equals(th.getUID().getId())) {
+ updateDeviceStatus(device, handler);
+ }
+ }
+ }
+ }
+
+ private void updateDeviceStatus(@Nullable OrbitBhyveDevice device, @Nullable OrbitBhyveSprinklerHandler handler) {
+ if (device != null && handler != null) {
+ handler.setDeviceOnline(device.isConnected());
+ handler.updateDeviceStatus(device.getStatus());
+ handler.updateSmartWatering(device.getWaterSenseMode());
+ return;
+ }
+ }
+
+ private void updateDeviceStatus(String deviceId) {
+ for (Thing th : getThing().getThings()) {
+ if (deviceId.equals(th.getUID().getId())) {
+ OrbitBhyveSprinklerHandler handler = (OrbitBhyveSprinklerHandler) th.getHandler();
+ OrbitBhyveDevice device = getDevice(deviceId);
+ updateDeviceStatus(device, handler);
+ }
+ }
+ }
+
+ private void updateDeviceProgramStatus(OrbitBhyveProgram program) {
+ for (Thing th : getThing().getThings()) {
+ if (program.getDeviceId().equals(th.getUID().getId())) {
+ OrbitBhyveSprinklerHandler handler = (OrbitBhyveSprinklerHandler) th.getHandler();
+ if (handler != null) {
+ handler.updateProgram(program);
+ }
+ }
+ }
+ }
+
+ private void disableZones(String deviceId) {
+ disableChannel(deviceId, "zone_");
+ }
+
+ private void disablePrograms(String deviceId) {
+ disableChannel(deviceId, "program_");
+ }
+
+ private void disableChannel(String deviceId, String name) {
+ for (Thing th : getThing().getThings()) {
+ if (deviceId.equals(th.getUID().getId())) {
+ for (Channel ch : th.getChannels()) {
+ if (ch.getUID().getId().startsWith(name)) {
+ updateState(ch.getUID(), OnOffType.OFF);
+ }
+ }
+ return;
+ }
+ }
+ }
+
+ private @Nullable Channel getThingChannel(String deviceId, int station) {
+ for (Thing th : getThing().getThings()) {
+ if (deviceId.equals(th.getUID().getId())) {
+ return th.getChannel("zone_" + station);
+ }
+ }
+ logger.debug("Cannot find zone: {} for device: {}", station, deviceId);
+ return null;
+ }
+
+ private @Nullable Channel getThingChannel(String deviceId, String name) {
+ for (Thing th : getThing().getThings()) {
+ if (deviceId.equals(th.getUID().getId())) {
+ return th.getChannel(name);
+ }
+ }
+ logger.debug("Cannot find channel: {} for device: {}", name, deviceId);
+ return null;
+ }
+
+ private @Nullable Session createSession() {
+ String url = BHYVE_WS_URL;
+ URI uri = URI.create(url);
+
+ try {
+ // The socket that receives events
+ OrbitBhyveSocket socket = new OrbitBhyveSocket(this);
+ // Attempt Connect
+ Future fut = webSocketClient.connect(socket, uri);
+ // Wait for Connect
+ return fut.get();
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot connect websocket client");
+ } catch (InterruptedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
+ }
+ return null;
+ }
+
+ private synchronized void initializeWebSocketSession() {
+ logger.debug("Initializing WebSocket session");
+ closeSession();
+ session = createSession();
+ Session localSession = session;
+ if (localSession != null) {
+ logger.debug("WebSocket connected!");
+ try {
+ String msg = "{\"event\":\"app_connection\",\"orbit_session_token\":\"" + sessionToken + "\"}";
+ logger.trace("sending message:\n {}", msg);
+ localSession.getRemote().sendString(msg);
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Cannot send hello string to web socket!");
+ }
+ }
+ }
+
+ private void closeSession() {
+ Session localSession = session;
+ if (localSession != null && localSession.isOpen()) {
+ localSession.close();
+ }
+ }
+
+ public void runZone(String deviceId, String zone, int time) {
+ String dateTime = format.format(new Date());
+ try {
+ ping();
+ Session localSession = session;
+ if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
+ localSession.getRemote()
+ .sendString("{\"event\":\"change_mode\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\""
+ + dateTime + "\",\"mode\":\"manual\",\"stations\":[{\"station\":" + zone
+ + ",\"run_time\":" + time + "}]}");
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Error during zone watering execution");
+ }
+ }
+
+ public void runProgram(String deviceId, String program) {
+ String dateTime = format.format(new Date());
+ try {
+ ping();
+ Session localSession = session;
+ if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
+ localSession.getRemote().sendString("{\"event\":\"change_mode\",\"mode\":\"manual\",\"program\":\""
+ + program + "\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\"" + dateTime + "\"}");
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Error during program watering execution");
+ }
+ }
+
+ public void enableProgram(OrbitBhyveProgram program, boolean enable) {
+ try {
+ String payLoad = "{\"sprinkler_timer_program\":{\"id\":\"" + program.getId() + "\",\"device_id\":\""
+ + program.getDeviceId() + "\",\"program\":\"" + program.getProgram() + "\",\"enabled\":" + enable
+ + "}}";
+ logger.debug("updating program {} with data {}", program.getProgram(), payLoad);
+ ContentResponse response = sendRequestBuilder(BHYVE_PROGRAMS + "/" + program.getId(), HttpMethod.PUT)
+ .content(new StringContentProvider(payLoad), "application/json; charset=utf-8").send();
+ if (response.getStatus() == 200) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Enable programs response: {}", response.getContentAsString());
+ }
+ return;
+ } else {
+ logger.debug("Returned status: {}", response.getStatus());
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ } catch (TimeoutException | ExecutionException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating programs");
+ } catch (InterruptedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating programs");
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public void setRainDelay(String deviceId, int delay) {
+ String dateTime = format.format(new Date());
+ try {
+ ping();
+ Session localSession = session;
+ if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
+ localSession.getRemote().sendString("{\"event\":\"rain_delay\",\"device_id\":\"" + deviceId
+ + "\",\"delay\":" + delay + ",\"timestamp\":\"" + dateTime + "\"}");
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during rain delay setting");
+ }
+ }
+
+ public void stopWatering(String deviceId) {
+ String dateTime = format.format(new Date());
+ try {
+ ping();
+ Session localSession = session;
+ if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
+ localSession.getRemote().sendString("{\"event\":\"change_mode\",\"device_id\":\"" + deviceId
+ + "\",\"timestamp\":\"" + dateTime + "\",\"mode\":\"manual\",\"stations\":[]}");
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during watering stopping");
+ }
+ }
+
+ public List getPrograms() {
+ try {
+ ContentResponse response = sendRequestBuilder(BHYVE_PROGRAMS, HttpMethod.GET).send();
+ if (response.getStatus() == 200) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Programs response: {}", response.getContentAsString());
+ }
+ OrbitBhyveProgram[] devices = gson.fromJson(response.getContentAsString(), OrbitBhyveProgram[].class);
+ return Arrays.asList(devices);
+ } else {
+ logger.debug("Returned status: {}", response.getStatus());
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ } catch (TimeoutException | ExecutionException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs");
+ } catch (InterruptedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs");
+ Thread.currentThread().interrupt();
+ }
+ return new ArrayList<>();
+ }
+
+ public void changeRunMode(String deviceId, String mode) {
+ String dateTime = format.format(new Date());
+ try {
+ ping();
+ Session localSession = session;
+ if (localSession != null && localSession.isOpen() && localSession.getRemote() != null) {
+ localSession.getRemote().sendString("{\"event\":\"change_mode\",\"mode\":\"" + mode
+ + "\",\"device_id\":\"" + deviceId + "\",\"timestamp\":\"" + dateTime + "\"}");
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during setting run mode");
+ }
+ }
+
+ public void setSmartWatering(String deviceId, boolean enable) {
+ OrbitBhyveDevice device = getDevice(deviceId);
+ if (device != null && device.getId().equals(deviceId)) {
+ device.setWaterSenseMode(enable ? "auto" : "off");
+ updateDevice(deviceId, gson.toJson(device));
+ }
+ }
+
+ private void updateDevice(String deviceId, String deviceString) {
+ String payload = "{\"device\":" + deviceString + "}";
+ logger.trace("New String: {}", payload);
+ try {
+ ContentResponse response = sendRequestBuilder(BHYVE_DEVICES + "/" + deviceId, HttpMethod.PUT)
+ .content(new StringContentProvider(payload), "application/json;charset=UTF-8").send();
+ if (logger.isTraceEnabled()) {
+ logger.trace("Device update response: {}", response.getContentAsString());
+ }
+ if (response.getStatus() != 200) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Update device response status: " + response.getStatus());
+ }
+ } catch (TimeoutException | ExecutionException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating device");
+ } catch (InterruptedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during updating device");
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveSprinklerHandler.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveSprinklerHandler.java
new file mode 100644
index 00000000000..546aeb84281
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveSprinklerHandler.java
@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.handler;
+
+import static org.openhab.binding.orbitbhyve.internal.OrbitBhyveBindingConstants.*;
+
+import java.util.HashMap;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveDevice;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveDeviceStatus;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveProgram;
+import org.openhab.binding.orbitbhyve.internal.model.OrbitBhyveZone;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link OrbitBhyveSprinklerHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveSprinklerHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(OrbitBhyveSprinklerHandler.class);
+
+ public OrbitBhyveSprinklerHandler(Thing thing) {
+ super(thing);
+ }
+
+ private int wateringTime = 5;
+ private HashMap programs = new HashMap<>();
+ private String deviceId = "";
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ OrbitBhyveBridgeHandler handler = getBridgeHandler();
+ if (handler != null) {
+ if (CHANNEL_CONTROL.equals(channelUID.getId()) && command instanceof OnOffType) {
+ String mode = OnOffType.ON.equals(command) ? "auto" : "off";
+ handler.changeRunMode(deviceId, mode);
+ return;
+ }
+ if (CHANNEL_SMART_WATERING.equals(channelUID.getId()) && command instanceof OnOffType) {
+ boolean enable = OnOffType.ON.equals(command);
+ handler.setSmartWatering(deviceId, enable);
+ return;
+ }
+ if (!channelUID.getId().startsWith("enable_program") && OnOffType.OFF.equals(command)) {
+ handler.stopWatering(deviceId);
+ return;
+ }
+ if (CHANNEL_WATERING_TIME.equals(channelUID.getId()) && command instanceof QuantityType) {
+ final QuantityType> value = ((QuantityType>) command).toUnit(Units.MINUTE);
+ if (value != null) {
+ wateringTime = value.intValue();
+ updateState(CHANNEL_WATERING_TIME, new DecimalType(wateringTime));
+ }
+ return;
+ }
+ if (channelUID.getId().startsWith("zone")) {
+ if (OnOffType.ON.equals(command)) {
+ handler.runZone(deviceId, channelUID.getId().replace("zone_", ""), wateringTime);
+ }
+ return;
+ }
+ if (channelUID.getId().startsWith("program")) {
+ if (OnOffType.ON.equals(command)) {
+ handler.runProgram(deviceId, channelUID.getId().replace("program_", ""));
+ }
+ return;
+ }
+ if (channelUID.getId().startsWith("enable_program") && command instanceof OnOffType) {
+ String id = channelUID.getId().replace("enable_program_", "");
+ OrbitBhyveProgram prog = programs.get(id);
+ if (prog != null) {
+ handler.enableProgram(prog, OnOffType.ON.equals(command));
+ } else {
+ logger.debug("Cannot get program id: {}", id);
+ }
+ return;
+ }
+ if (CHANNEL_RAIN_DELAY.equals(channelUID.getId()) && command instanceof DecimalType) {
+ final QuantityType> value = ((QuantityType>) command).toUnit(Units.HOUR);
+ if (value != null) {
+ handler.setRainDelay(deviceId, value.intValue());
+ }
+
+ }
+ }
+ }
+
+ private String getSprinklerId() {
+ return getThing().getConfiguration().get("id") != null ? getThing().getConfiguration().get("id").toString()
+ : "";
+ }
+
+ private @Nullable OrbitBhyveBridgeHandler getBridgeHandler() {
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ return (OrbitBhyveBridgeHandler) bridge.getHandler();
+ }
+ return null;
+ }
+
+ @Override
+ public void initialize() {
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ logger.debug("Initializing, bridge is {}", bridge.getStatus());
+ if (ThingStatus.ONLINE == bridge.getStatus()) {
+ doInit();
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
+ }
+ }
+
+ private synchronized void doInit() {
+ OrbitBhyveBridgeHandler handler = getBridgeHandler();
+ if (handler != null) {
+ deviceId = getSprinklerId();
+ if ("".equals(deviceId)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Sprinkler id is missing!");
+ } else {
+ OrbitBhyveDevice device = handler.getDevice(deviceId);
+ if (device != null) {
+ setDeviceOnline(device.isConnected());
+ createChannels(device.getZones());
+ updateDeviceStatus(device.getStatus());
+ }
+ List programs = handler.getPrograms();
+ for (OrbitBhyveProgram program : programs) {
+ if (deviceId.equals(program.getDeviceId())) {
+ cacheProgram(program);
+ createProgram(program);
+ }
+ }
+
+ updateState(CHANNEL_WATERING_TIME, new DecimalType(wateringTime));
+ logger.debug("Finished initializing of sprinkler!");
+ }
+ }
+ }
+
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+ doInit();
+ }
+ }
+
+ private synchronized void cacheProgram(OrbitBhyveProgram program) {
+ if (!programs.containsKey(program.getProgram())) {
+ programs.put(program.getProgram(), program);
+ }
+ }
+
+ public void updateDeviceStatus(OrbitBhyveDeviceStatus status) {
+ if (!status.getMode().isEmpty()) {
+ updateState(CHANNEL_MODE, new StringType(status.getMode()));
+ updateState(CHANNEL_CONTROL, "off".equals(status.getMode()) ? OnOffType.OFF : OnOffType.ON);
+ }
+ if (!status.getNextStartTime().isEmpty()) {
+ DateTimeType dt = new DateTimeType(status.getNextStartTime());
+ updateState(CHANNEL_NEXT_START, dt);
+ logger.debug("Next start time: {}", status.getNextStartTime());
+ }
+ updateState(CHANNEL_RAIN_DELAY, new DecimalType(status.getDelay()));
+ }
+
+ private void createProgram(OrbitBhyveProgram program) {
+ String channelName = "program_" + program.getProgram();
+ if (thing.getChannel(channelName) == null) {
+ logger.debug("Creating channel for program: {} with name: {}", program.getProgram(), program.getName());
+ createProgramChannel(channelName, "Switch", "Program " + program.getName());
+ }
+ String enableChannelName = "enable_" + channelName;
+ if (thing.getChannel(enableChannelName) == null) {
+ logger.debug("Creating enable channel for program: {} with name: {}", program.getProgram(),
+ program.getName());
+ createProgramChannel(enableChannelName, "Switch", "Enable program " + program.getName());
+ }
+ Channel ch = thing.getChannel(enableChannelName);
+ if (ch != null) {
+ updateState(ch.getUID(), program.isEnabled() ? OnOffType.ON : OnOffType.OFF);
+ }
+ }
+
+ private void createProgramChannel(String name, String type, String label) {
+ ChannelTypeUID program = new ChannelTypeUID(BINDING_ID, "program");
+ createChannel(name, type, label, program);
+ }
+
+ private void createChannels(List zones) {
+ for (OrbitBhyveZone zone : zones) {
+ String channelName = "zone_" + zone.getStation();
+ if (thing.getChannel(channelName) == null) {
+ logger.debug("Creating channel for zone: {} with name: {}", zone.getStation(), zone.getName());
+ createZoneChannel(channelName, "Switch", "Zone " + zone.getName());
+ }
+ }
+ }
+
+ private void createZoneChannel(String name, String type, String label) {
+ ChannelTypeUID zone = new ChannelTypeUID(BINDING_ID, "zone");
+ createChannel(name, type, label, zone);
+ }
+
+ private void createChannel(String name, String type, String label, ChannelTypeUID typeUID) {
+ ThingBuilder thingBuilder = editThing();
+ Channel channel = ChannelBuilder.create(new ChannelUID(thing.getUID(), name), type).withLabel(label)
+ .withType(typeUID).build();
+ thingBuilder.withChannel(channel);
+ updateThing(thingBuilder.build());
+ }
+
+ public void setDeviceOnline(boolean connected) {
+ if (!connected) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Not connected to Orbit BHyve Cloud");
+ } else {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ public void updateProgram(OrbitBhyveProgram program) {
+ String enableChannelName = "enable_program_" + program.getProgram();
+ Channel ch = thing.getChannel(enableChannelName);
+ if (ch != null) {
+ updateState(ch.getUID(), program.isEnabled() ? OnOffType.ON : OnOffType.OFF);
+ }
+ }
+
+ public void updateSmartWatering(String senseMode) {
+ updateState(CHANNEL_SMART_WATERING, ("auto".equals(senseMode)) ? OnOffType.ON : OnOffType.OFF);
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDevice.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDevice.java
new file mode 100644
index 00000000000..3a79153432b
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDevice.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link OrbitBhyveDevice} holds information about a B-Hyve
+ * device.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveDevice {
+ String name = "";
+ String type = "";
+ String id = "";
+ List zones = new ArrayList<>();
+ OrbitBhyveDeviceStatus status = new OrbitBhyveDeviceStatus();
+
+ @SerializedName("is_connected")
+ boolean isConnected = false;
+
+ @SerializedName("hardware_version")
+ String hwVersion = "";
+
+ @SerializedName("firmware_version")
+ String fwVersion = "";
+
+ @SerializedName("mac_address")
+ String macAddress = "";
+
+ @SerializedName("num_stations")
+ int numStations = 0;
+
+ @SerializedName("last_connected_at")
+ String lastConnectedAt = "";
+
+ JsonObject location = new JsonObject();
+
+ @SerializedName("restricted_frequency")
+ JsonObject restrictedFrequency = new JsonObject();
+
+ @SerializedName("suggested_start_time")
+ String suggestedStartTime = "";
+
+ JsonObject timezone = new JsonObject();
+
+ @SerializedName("water_sense_mode")
+ String waterSenseMode = "";
+
+ @SerializedName("wifi_version")
+ int wifiVersion = 0;
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public boolean isConnected() {
+ return isConnected;
+ }
+
+ public String getHwVersion() {
+ return hwVersion;
+ }
+
+ public String getFwVersion() {
+ return fwVersion;
+ }
+
+ public String getMacAddress() {
+ return macAddress;
+ }
+
+ public int getNumStations() {
+ return numStations;
+ }
+
+ public List getZones() {
+ return zones;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public OrbitBhyveDeviceStatus getStatus() {
+ return status;
+ }
+
+ public String getWaterSenseMode() {
+ return waterSenseMode;
+ }
+
+ public void setWaterSenseMode(String waterSenseMode) {
+ this.waterSenseMode = waterSenseMode;
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDeviceStatus.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDeviceStatus.java
new file mode 100644
index 00000000000..4d0fd54a998
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDeviceStatus.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link OrbitBhyveDeviceStatus} holds information about a B-Hyve
+ * device status.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveDeviceStatus {
+ @SerializedName("run_mode")
+ String mode = "";
+
+ @SerializedName("next_start_time")
+ String nextStartTime = "";
+
+ @SerializedName("rain_delay")
+ int delay = 0;
+
+ @SerializedName("rain_delay_started_at")
+ String rainDelayStartedAt = "";
+
+ public String getMode() {
+ return mode;
+ }
+
+ public String getNextStartTime() {
+ return nextStartTime;
+ }
+
+ public int getDelay() {
+ return delay;
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveProgram.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveProgram.java
new file mode 100644
index 00000000000..b7c09f7ad3a
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveProgram.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link OrbitBhyveProgram} holds information about a B-Hyve
+ * device programs.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveProgram {
+ @SerializedName("device_id")
+ String deviceId = "";
+
+ String program = "";
+ String name = "";
+ String id = "";
+ boolean enabled = false;
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public String getProgram() {
+ return program;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSessionResponse.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSessionResponse.java
new file mode 100644
index 00000000000..b9dd8aaa010
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSessionResponse.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link OrbitBhyveSessionResponse} holds information about a B-Hyve
+ * session response.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveSessionResponse {
+ @SerializedName("orbit_session_token")
+ String orbitSessionToken = "";
+
+ public String getOrbitSessionToken() {
+ return orbitSessionToken;
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSocketEvent.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSocketEvent.java
new file mode 100644
index 00000000000..180498128e8
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSocketEvent.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link OrbitBhyveSocketEvent} holds information about a B-Hyve
+ * event received on web socket.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveSocketEvent {
+ String event = "";
+ String mode = "";
+ JsonElement program = new JsonObject();
+ int delay = 0;
+
+ @SerializedName("device_id")
+ String deviceId = "";
+
+ @SerializedName("current_station")
+ int station = 0;
+
+ public String getEvent() {
+ return event;
+ }
+
+ public String getMode() {
+ return mode;
+ }
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public int getStation() {
+ return station;
+ }
+
+ public JsonElement getProgram() {
+ return program;
+ }
+
+ public int getDelay() {
+ return delay;
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveZone.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveZone.java
new file mode 100644
index 00000000000..c7ca84947d3
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveZone.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonArray;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link OrbitBhyveZone} holds information about a B-Hyve
+ * zone.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveZone {
+ String name = "";
+ int station = 0;
+
+ @SerializedName("catch_cup_run_time")
+ int catchCupRunTime = 0;
+
+ @SerializedName("catch_cup_volumes")
+ JsonArray catchCupVolumes = new JsonArray();
+
+ @SerializedName("num_sprinklers")
+ int numSprinklers = 0;
+
+ @SerializedName("landscape_type")
+ @Nullable
+ String landscapeType;
+
+ @SerializedName("soil_type")
+ @Nullable
+ String soilType;
+
+ @SerializedName("sprinkler_type")
+ @Nullable
+ String sprinklerType;
+
+ @SerializedName("sun_shade")
+ @Nullable
+ String sunShade;
+
+ @SerializedName("slope_grade")
+ int slopeGrade = 0;
+
+ @SerializedName("image_url")
+ String imageUrl = "";
+
+ @SerializedName("smart_watering_enabled")
+ boolean smartWateringEnabled = false;
+
+ public String getName() {
+ return name;
+ }
+
+ public int getStation() {
+ return station;
+ }
+
+ public boolean isSmartWateringEnabled() {
+ return smartWateringEnabled;
+ }
+
+ public void setSmartWateringEnabled(boolean smartWateringEnabled) {
+ this.smartWateringEnabled = smartWateringEnabled;
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/net/OrbitBhyveSocket.java b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/net/OrbitBhyveSocket.java
new file mode 100644
index 00000000000..2a77e53bf2e
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/net/OrbitBhyveSocket.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.orbitbhyve.internal.net;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.openhab.binding.orbitbhyve.internal.handler.OrbitBhyveBridgeHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link OrbitBhyveSocket} class defines websocket used for connection with
+ * the Orbit B-Hyve cloud.
+ *
+ * @author Ondrej Pecta - Initial contribution
+ */
+@NonNullByDefault
+public class OrbitBhyveSocket extends WebSocketAdapter {
+ private final Logger logger = LoggerFactory.getLogger(OrbitBhyveSocket.class);
+ private OrbitBhyveBridgeHandler handler;
+
+ public OrbitBhyveSocket(OrbitBhyveBridgeHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void onWebSocketText(@Nullable String message) {
+ super.onWebSocketText(message);
+ if (message != null) {
+ logger.trace("Got message: {}", message);
+ handler.processStatusResponse(message);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..df5292043c0
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Orbit B-hyve Binding
+ This is the binding for Orbit B-hyve Wi-Fi irrigation systems.
+
+
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 00000000000..568af236e6b
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ email
+ This is a login to your B-hyve account.
+
+
+
+ password
+ This is a password to your B-hyve account.
+
+
+
+ Specifies the refresh time in seconds for polling data from Orbit cloud
+ 30
+
+
+
+
+
+
+ The identifier of the Orbit sprinkler device
+
+
+
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644
index 00000000000..b5dcf1a76ed
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/bridge.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Bridge for Orbit B-hyve Binding
+
+
+
+
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/channels.xml
new file mode 100644
index 00000000000..f4efac85493
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/channels.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+ String
+
+ Channel representing mode of Orbit B-hyve Device (auto/manual)
+
+
+
+
+
+
+
+
+ DateTime
+
+ Channel representing start time of the next watering
+
+
+
+ Number:Time
+
+ Channel representing rain delay in hours
+
+
+
+ Number:Time
+
+ Channel representing the manual zone watering time in minutes
+
+
+
+ Switch
+
+ Channel for enabling/disabling the sprinkler (ON/OFF)
+
+
+ Switch
+
+ Channel for enabling/disabling the smart watering mode
+
+
+ Switch
+
+ Dynamic channel representing a program
+
+
+ Switch
+
+ Dynamic channel representing a zone
+
+
diff --git a/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/sprinkler.xml b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/sprinkler.xml
new file mode 100644
index 00000000000..f3040b067b5
--- /dev/null
+++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/sprinkler.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+ Orbit B-hyve Sprinkler
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index fa0385512ab..e0caa989a43 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -254,6 +254,7 @@
org.openhab.binding.openweathermap
org.openhab.binding.openwebnet
org.openhab.binding.oppo
+ org.openhab.binding.orbitbhyve
org.openhab.binding.orvibo
org.openhab.binding.paradoxalarm
org.openhab.binding.pentair