]> git.basschouten.com Git - openhab-addons.git/commitdiff
[orbitbhyve] Initial contribution (#10426)
authorOndrej Pecta <opecta@gmail.com>
Sat, 31 Jul 2021 10:01:22 +0000 (12:01 +0200)
committerGitHub <noreply@github.com>
Sat, 31 Jul 2021 10:01:22 +0000 (12:01 +0200)
* [orbitbhyve] initial contribution

Signed-off-by: Ondrej Pecta <opecta@gmail.com>
* [orbitbhyve] improvements based on code review

Signed-off-by: Ondrej Pecta <opecta@gmail.com>
* [orbitbhyve] next bunch of fixes related to code review

Signed-off-by: Ondrej Pecta <opecta@gmail.com>
* Update bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveHandlerFactory.java

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
25 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.orbitbhyve/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/README.md [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/discovery/OrbitBhyveDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveSprinklerHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveDeviceStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveProgram.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSessionResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveSocketEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/model/OrbitBhyveZone.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/net/OrbitBhyveSocket.java [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/bridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/channels.xml [new file with mode: 0644]
bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/sprinkler.xml [new file with mode: 0644]
bundles/pom.xml

index bd8485467bc6358ca8ab14c3aa67a90543f5d52f..a78c5cbb04ed7a9d5bd8c920181243b2da49b6ea 100644 (file)
 /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
index 7830bf7d35cf681ca90b2e080b5b8d916ec6844b..f4184174b7c545d0321b33b1d1cb9d2a129e4988 100644 (file)
       <artifactId>org.openhab.binding.oppo</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.orbitbhyve</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.orvibo</artifactId>
diff --git a/bundles/org.openhab.binding.orbitbhyve/NOTICE b/bundles/org.openhab.binding.orbitbhyve/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..9d0d887
--- /dev/null
@@ -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" <bhyve>   (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:control" }  
+Switch IrrigationSmartWatering "Smart watering" <bhyve>        (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:smart_watering" }  
+Switch Irrigation1 "Zone 1" <water>    (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_1" }  
+Switch Irrigation2 "Zone 2" <water>    (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_2" }  
+Switch Irrigation3 "Zone 3" <water>    (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_3" }  
+Switch Irrigation4 "Zone 4" <water>    (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:zone_4" }  
+Switch IrrigationP1 "Run program A" <program>  (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:program_a" }  
+Switch IrrigationP1Enable "Schedule program A" <program>       (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:enable_program_a" }  
+String IrrigationMode "Irrigation mode [%s]" <water>   (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:mode" }  
+Number IrrigationTime "Irrigation time [%d min]" <clock>       (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:watering_time" }  
+Number IrrigationRainDelay "Rain delay [%d h]" <hourglass>     (Out_Irrigation) { channel="orbitbhyve:sprinkler:mybridge:indoor_timer:rain_delay" }  
+DateTime IrrigationNextStart "Next start A [%1$td.%1$tm.%1$tY %1$tR]" <clock>  (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 (file)
index 0000000..332ae97
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.orbitbhyve</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Orbit B-hyve Binding</name>
+
+</project>
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 (file)
index 0000000..23c479a
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.orbitbhyve-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-orbitbhyve" description="Orbit B-hyve Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.orbitbhyve/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..8886596
--- /dev/null
@@ -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 (file)
index 0000000..24a6de6
--- /dev/null
@@ -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 (file)
index 0000000..c392d5f
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..68974ee
--- /dev/null
@@ -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<OrbitBhyveDevice> 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<String, Object> 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<ThingTypeUID> 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 (file)
index 0000000..73e6685
--- /dev/null
@@ -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<ConfigStatusMessage> getConfigStatus() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> 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<OrbitBhyveDevice> 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<OrbitBhyveDevice> 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<Session> 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<OrbitBhyveProgram> 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 (file)
index 0000000..546aeb8
--- /dev/null
@@ -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<String, OrbitBhyveProgram> 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<OrbitBhyveProgram> 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<OrbitBhyveZone> 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 (file)
index 0000000..3a79153
--- /dev/null
@@ -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<OrbitBhyveZone> 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<OrbitBhyveZone> 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 (file)
index 0000000..4d0fd54
--- /dev/null
@@ -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 (file)
index 0000000..b7c09f7
--- /dev/null
@@ -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 (file)
index 0000000..b9dd8aa
--- /dev/null
@@ -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 (file)
index 0000000..1804981
--- /dev/null
@@ -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 (file)
index 0000000..c7ca849
--- /dev/null
@@ -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 (file)
index 0000000..2a77e53
--- /dev/null
@@ -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 (file)
index 0000000..df52920
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="orbitbhyve" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Orbit B-hyve Binding</name>
+       <description>This is the binding for Orbit B-hyve Wi-Fi irrigation systems.</description>
+
+</binding:binding>
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 (file)
index 0000000..568af23
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="bridge-type:orbitbhyve:bridge">
+               <parameter name="email" type="text" required="true">
+                       <label>Email</label>
+                       <context>email</context>
+                       <description>This is a login to your B-hyve account.</description>
+               </parameter>
+               <parameter name="password" type="text" required="true">
+                       <label>Password</label>
+                       <context>password</context>
+                       <description>This is a password to your B-hyve account.</description>
+               </parameter>
+               <parameter name="refresh" type="integer" required="false" min="10">
+                       <label>Refresh</label>
+                       <description>Specifies the refresh time in seconds for polling data from Orbit cloud</description>
+                       <default>30</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:orbitbhyve:sprinkler">
+               <parameter name="id" type="text" required="true">
+                       <label>Sprinkler Device ID</label>
+                       <description>The identifier of the Orbit sprinkler device</description>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
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 (file)
index 0000000..b5dcf1a
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="orbitbhyve"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Bridge Type -->
+       <bridge-type id="bridge">
+               <label>Bridge</label>
+               <description>Bridge for Orbit B-hyve Binding</description>
+               <config-description-ref uri="bridge-type:orbitbhyve:bridge"/>
+       </bridge-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..f4efac8
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="orbitbhyve"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Mode Channel Type -->
+       <channel-type id="mode">
+               <item-type>String</item-type>
+               <label>Irrigation Mode</label>
+               <description>Channel representing mode of Orbit B-hyve Device (auto/manual)</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="auto">Auto</option>
+                               <option value="manual">Manual</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="next_start">
+               <item-type>DateTime</item-type>
+               <label>Next Watering Time</label>
+               <description>Channel representing start time of the next watering</description>
+               <state readOnly="true"></state>
+       </channel-type>
+       <channel-type id="rain_delay">
+               <item-type>Number:Time</item-type>
+               <label>Rain Delay</label>
+               <description>Channel representing rain delay in hours</description>
+               <state pattern="%d h"></state>
+       </channel-type>
+       <channel-type id="watering_time">
+               <item-type>Number:Time</item-type>
+               <label>Zone Watering Time</label>
+               <description>Channel representing the manual zone watering time in minutes</description>
+               <state max="240" min="0" pattern="%d min"></state>
+       </channel-type>
+       <channel-type id="control">
+               <item-type>Switch</item-type>
+               <label>Sprinkler State Control</label>
+               <description>Channel for enabling/disabling the sprinkler (ON/OFF)</description>
+       </channel-type>
+       <channel-type id="smart_watering">
+               <item-type>Switch</item-type>
+               <label>Smart Watering Control</label>
+               <description>Channel for enabling/disabling the smart watering mode</description>
+       </channel-type>
+       <channel-type id="program">
+               <item-type>Switch</item-type>
+               <label>Program Channel</label>
+               <description>Dynamic channel representing a program</description>
+       </channel-type>
+       <channel-type id="zone">
+               <item-type>Switch</item-type>
+               <label>Zone Channel</label>
+               <description>Dynamic channel representing a zone</description>
+       </channel-type>
+</thing:thing-descriptions>
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 (file)
index 0000000..f3040b0
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="orbitbhyve"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="sprinkler">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>Sprinkler</label>
+               <description>Orbit B-hyve Sprinkler</description>
+               <channels>
+                       <channel id="control" typeId="control"/>
+                       <channel id="mode" typeId="mode"/>
+                       <channel id="smart_watering" typeId="smart_watering"/>
+                       <channel id="next_start" typeId="next_start"/>
+                       <channel id="rain_delay" typeId="rain_delay"/>
+                       <channel id="watering_time" typeId="watering_time"/>
+               </channels>
+               <config-description-ref uri="thing-type:orbitbhyve:sprinkler"/>
+       </thing-type>
+
+</thing:thing-descriptions>
index fa0385512abd789809ae0a2f4ec9605a9ca67529..e0caa989a43921e21dc222e1bf6bfb9ca0721aea 100644 (file)
     <module>org.openhab.binding.openweathermap</module>
     <module>org.openhab.binding.openwebnet</module>
     <module>org.openhab.binding.oppo</module>
+    <module>org.openhab.binding.orbitbhyve</module>
     <module>org.openhab.binding.orvibo</module>
     <module>org.openhab.binding.paradoxalarm</module>
     <module>org.openhab.binding.pentair</module>