diff --git a/CODEOWNERS b/CODEOWNERS index 300354bcf8e5..de40aed6bfb6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -221,6 +221,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/bundles/org.openhab.binding.orbitbhyve/NOTICE b/bundles/org.openhab.binding.orbitbhyve/NOTICE new file mode 100644 index 000000000000..38d625e34923 --- /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 000000000000..1fe81204c9a6 --- /dev/null +++ b/bundles/org.openhab.binding.orbitbhyve/README.md @@ -0,0 +1,90 @@ +# 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. + +## Binding Configuration + +This binding does not require any configuration on the binding level. + +## 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 | This channel manages the current rain delay in hours | +| watering_time | Number | 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) | +| control | String | This channel manages the manual zone watering time in minutes | + + +## 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] +``` \ No newline at end of file diff --git a/bundles/org.openhab.binding.orbitbhyve/pom.xml b/bundles/org.openhab.binding.orbitbhyve/pom.xml new file mode 100644 index 000000000000..003423b2c61a --- /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 :: OrbitBhyve 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 000000000000..b3085e6deba4 --- /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 000000000000..888659641b18 --- /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 000000000000..24a6de6356d8 --- /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 000000000000..d9e79aaa6f81 --- /dev/null +++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/OrbitBhyveHandlerFactory.java @@ -0,0 +1,87 @@ +/** + * 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.Arrays; +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 = new HashSet<>( + Arrays.asList(THING_TYPE_BRIDGE, THING_TYPE_SPRINKLER)); + + /** + * the shared http client + */ + private @NonNullByDefault({}) HttpClient httpClient; + + /** + * the shared web socket client + */ + private @NonNullByDefault({}) 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 000000000000..68974ee2564e --- /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 000000000000..355283659afb --- /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.isCancelled() || localFuture.isDone())) { + 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); + return false; + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Exception during login", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + if (e instanceof InterruptedException) { + 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\"}"); + } catch (IOException e) { + logger.debug("Error sending ping to a web socket", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "web socket communication error"); + } + } 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 { + logger.debug("Returned status: {}", response.getStatus()); + updateStatus(ThingStatus.OFFLINE); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Error during getting devices", e); + updateStatus(ThingStatus.OFFLINE); + if (e instanceof InterruptedException) { + 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 (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Error during getting device: {}", deviceId, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error during getting device info"); + if (e instanceof InterruptedException) { + 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) { + Runnable runnableUpdateDeviceStatus = () -> updateDeviceStatus(event.getDeviceId()); + 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); + } + scheduler.schedule(runnableUpdateDeviceStatus, 3, TimeUnit.SECONDS); + break; + case "rain_delay": + scheduler.schedule(runnableUpdateDeviceStatus, 3, TimeUnit.SECONDS); + 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); + scheduler.schedule(() -> updateDeviceStatus(program.getDeviceId()), 3, TimeUnit.SECONDS); + } + break; + default: + logger.debug("Received event: {}", event.getEvent()); + } + } + + 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); + if (device != null && handler != null) { + handler.setDeviceOnline(device.isConnected()); + handler.updateDeviceStatus(device.getStatus()); + handler.updateSmartWatering(device.getWaterSenseMode()); + return; + } + } + } + } + + 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) { + logger.debug("Cannot connect websocket client", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot connect websocket client"); + } catch (InterruptedException e) { + logger.debug("Cannot create websocket session", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session"); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.debug("Cannot create websocket session", 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) { + logger.debug("Cannot send hello string to web socket!", 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) { + logger.debug("Error during zone watering execution", 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) { + logger.debug("Error during program watering execution", 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 (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Error during updating program", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs"); + if (e instanceof InterruptedException) { + 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) { + logger.debug("Error during rain delay setting", 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) { + logger.debug("Error during watering stopping", 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 (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Error during getting programs", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error during getting programs"); + if (e instanceof InterruptedException) { + 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) { + logger.debug("Error during setting run mode", 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 (response.getStatus() == 200) { + if (logger.isTraceEnabled()) { + logger.trace("Device update response: {}", response.getContentAsString()); + } + } else { + logger.debug("Returned status: {}", response.getStatus()); + if (logger.isTraceEnabled()) { + logger.trace("Device update response: {}", response.getContentAsString()); + } + updateStatus(ThingStatus.OFFLINE); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Error during updating device", e); + updateStatus(ThingStatus.OFFLINE); + if (e instanceof InterruptedException) { + 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 000000000000..868f01c552e1 --- /dev/null +++ b/bundles/org.openhab.binding.orbitbhyve/src/main/java/org/openhab/binding/orbitbhyve/internal/handler/OrbitBhyveSprinklerHandler.java @@ -0,0 +1,261 @@ +/** + * 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.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.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 DecimalType) { + wateringTime = ((DecimalType) command).intValue(); + updateState(CHANNEL_WATERING_TIME, (DecimalType) command); + 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) { + handler.setRainDelay(deviceId, ((DecimalType) command).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, (senseMode.equals("auto")) ? 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 000000000000..3a79153432b0 --- /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 000000000000..4d0fd54a998c --- /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 000000000000..b7c09f7ad3a5 --- /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 000000000000..b9dd8aaa0109 --- /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 000000000000..180498128e88 --- /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 000000000000..c7ca84947d3d --- /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 000000000000..bb1ab9e99e98 --- /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(OrbitBhyveBridgeHandler.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 000000000000..22a1f7997fba --- /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. + + 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 000000000000..d11f64fc12b6 --- /dev/null +++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,30 @@ + + + + + + + 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 events 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 000000000000..f0868230740a --- /dev/null +++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/bridge.xml @@ -0,0 +1,13 @@ + + + + + + + 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 000000000000..5397da31a5a9 --- /dev/null +++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/channels.xml @@ -0,0 +1,52 @@ + + + + + + String + + Channel representing mode of Orbit B-Hyve Device (auto/manual) + + + + DateTime + + Channel representing start time of the next watering + + + + Integer + + Channel representing rain delay in hours + + + + Integer + + 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 000000000000..c26009da2120 --- /dev/null +++ b/bundles/org.openhab.binding.orbitbhyve/src/main/resources/OH-INF/thing/sprinkler.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 488e5ba416b8..e9c21ac00ca6 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -251,6 +251,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