[hydrawise] Migrate to new GraphQL based API (#10947)

* [hydrawise] Migrated to new GraphQL based API

Fixes #7261

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

* Addressed PR comments.

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

* Address PR review comments.

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
Dan Cunningham
2021-08-01 11:03:37 -07:00
committed by GitHub
parent f25cc8d14a
commit e465155d84
65 changed files with 2369 additions and 947 deletions

View File

@@ -23,44 +23,57 @@ import org.openhab.core.thing.ThingTypeUID;
*/
@NonNullByDefault
public class HydrawiseBindingConstants {
private static final String BINDING_ID = "hydrawise";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CLOUD = new ThingTypeUID(BINDING_ID, "cloud");
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
public static final ThingTypeUID THING_TYPE_LOCAL = new ThingTypeUID(BINDING_ID, "local");
public static final String BASE_IMAGE_URL = "https://app.hydrawise.com/config/images/";
public static final String CONFIG_USERNAME = "userName";
public static final String CONFIG_PASSWORD = "password";
public static final String CONFIG_REFRESHTOKEN = "refreshToken";
public static final String CONFIG_CONTROLLER_ID = "controllerId";
public static final String CHANNEL_GROUP_CONTROLLER_SYSTEM = "system";
public static final String CHANNEL_CONTROLLER_NAME = "name";
public static final String CHANNEL_CONTROLLER_LAST_CONTACT = "lastcontact";
public static final String CHANNEL_CONTROLLER_STATUS = "status";
public static final String CHANNEL_CONTROLLER_SUMMARY = "summary";
public static final String CHANNEL_CONTROLLER_ONLINE = "online";
public static final String CHANNEL_GROUP_ALLZONES = "allzones";
public static final String CHANNEL_ZONE_RUN_CUSTOM = "runcustom";
public static final String CHANNEL_ZONE_RUN = "run";
public static final String CHANNEL_ZONE_STOP = "stop";
public static final String CHANNEL_ZONE_SUSPEND = "suspend";
public static final String CHANNEL_ZONE_NAME = "name";
public static final String CHANNEL_ZONE_ICON = "icon";
public static final String CHANNEL_ZONE_LAST_WATER = "lastwater";
public static final String CHANNEL_ZONE_TIME = "time";
public static final String CHANNEL_ZONE_STARTTIME = "starttime";
public static final String CHANNEL_ZONE_DURATION = "duration";
public static final String CHANNEL_ZONE_TYPE = "type";
public static final String CHANNEL_ZONE_RUN = "run";
public static final String CHANNEL_ZONE_RUN_CUSTOM = "runcustom";
public static final String CHANNEL_ZONE_NEXT_RUN_TIME_TIME = "nextruntime";
public static final String CHANNEL_ZONE_SUSPEND = "suspend";
public static final String CHANNEL_ZONE_SUSPENDUNTIL = "suspenduntil";
public static final String CHANNEL_ZONE_SUMMARY = "summary";
public static final String CHANNEL_ZONE_TIME_LEFT = "timeleft";
public static final String CHANNEL_RUN_ALL_ZONES = "runall";
public static final String CHANNEL_STOP_ALL_ZONES = "stopall";
public static final String CHANNEL_SUSPEND_ALL_ZONES = "suspendall";
public static final String CHANNEL_SENSOR_NAME = "name";
public static final String CHANNEL_SENSOR_INPUT = "input";
public static final String CHANNEL_SENSOR_MODE = "mode";
public static final String CHANNEL_SENSOR_TIMER = "timer";
public static final String CHANNEL_SENSOR_DELAY = "delay";
public static final String CHANNEL_SENSOR_OFFTIMER = "offtimer";
public static final String CHANNEL_SENSOR_OFFLEVEL = "offlevel";
public static final String CHANNEL_SENSOR_ACTIVE = "active";
public static final String CHANNEL_SENSOR_WATERFLOW = "waterflow";
public static final String CHANNEL_FORECAST_TEMPERATURE_HIGH = "temperaturehigh";
public static final String CHANNEL_FORECAST_TEMPERATURE_LOW = "temperaturelow";
public static final String CHANNEL_FORECAST_CONDITIONS = "conditions";
public static final String CHANNEL_FORECAST_DAY = "day";
public static final String CHANNEL_FORECAST_TIME = "time";
public static final String CHANNEL_FORECAST_HUMIDITY = "humidity";
public static final String CHANNEL_FORECAST_WIND = "wind";
public static final String CHANNEL_FORECAST_ICON = "icon";
public static final String CHANNEL_FORECAST_EVAPOTRANSPRIATION = "evapotranspiration";
public static final String CHANNEL_FORECAST_PRECIPITATION = "precipitation";
public static final String CHANNEL_FORECAST_PROBABILITYOFPRECIPITATION = "probabilityofprecipitation";
public static final String PROPERTY_CONTROLLER_ID = "controller";
public static final String PROPERTY_NAME = "name";
public static final String PROPERTY_DESCRIPTION = "description";

View File

@@ -1,243 +0,0 @@
/**
* 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.hydrawise.internal;
import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseCloudApiClient;
import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
import org.openhab.binding.hydrawise.internal.api.model.Controller;
import org.openhab.binding.hydrawise.internal.api.model.CustomerDetailsResponse;
import org.openhab.binding.hydrawise.internal.api.model.Forecast;
import org.openhab.binding.hydrawise.internal.api.model.Relay;
import org.openhab.binding.hydrawise.internal.api.model.StatusScheduleResponse;
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.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HydrawiseCloudHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class HydrawiseCloudHandler extends HydrawiseHandler {
/**
* 74.2 F
*/
private static final Pattern TEMPERATURE_PATTERN = Pattern.compile("^(\\d{1,3}.?\\d?)\\s([C,F])");
/**
* 9 mph
*/
private static final Pattern WIND_SPEED_PATTERN = Pattern.compile("^(\\d{1,3})\\s([a-z]{3})");
private final Logger logger = LoggerFactory.getLogger(HydrawiseCloudHandler.class);
private HydrawiseCloudApiClient client;
private int controllerId;
public HydrawiseCloudHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.client = new HydrawiseCloudApiClient(httpClient);
}
@Override
protected void configure()
throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException {
HydrawiseCloudConfiguration configuration = getConfig().as(HydrawiseCloudConfiguration.class);
this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS);
client.setApiKey(configuration.apiKey);
CustomerDetailsResponse customerDetails = client.getCustomerDetails();
List<Controller> controllers = customerDetails.controllers;
if (controllers.isEmpty()) {
throw new NotConfiguredException("No controllers found on account");
}
Controller controller = null;
// try and use ID from user configuration
if (configuration.controllerId != null) {
controller = getController(configuration.controllerId.intValue(), controllers);
if (controller == null) {
throw new NotConfiguredException("No controller found for id " + configuration.controllerId);
}
} else {
// try and use ID from saved property
String controllerId = getThing().getProperties().get(PROPERTY_CONTROLLER_ID);
if (controllerId != null && !controllerId.isBlank()) {
try {
controller = getController(Integer.parseInt(controllerId), controllers);
} catch (NumberFormatException e) {
logger.debug("Can not parse property vaue {}", controllerId);
}
}
// use current controller ID
if (controller == null) {
controller = getController(customerDetails.controllerId, controllers);
}
}
if (controller == null) {
throw new NotConfiguredException("No controller found");
}
controllerId = controller.controllerId.intValue();
updateControllerProperties(controller);
logger.debug("Controller id {}", controllerId);
}
/**
* Poll the controller for updates.
*/
@Override
protected void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException {
List<Controller> controllers = client.getCustomerDetails().controllers;
Controller controller = getController(controllerId, controllers);
if (controller != null && !controller.online) {
throw new HydrawiseConnectionException("Controller is offline");
}
StatusScheduleResponse status = client.getStatusSchedule(controllerId);
updateSensors(status);
updateForecast(status);
updateZones(status);
}
@Override
protected void sendRunCommand(int seconds, @Nullable Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
if (relay != null) {
client.runRelay(seconds, relay.relayId);
}
}
@Override
protected void sendRunCommand(@Nullable Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
if (relay != null) {
client.runRelay(relay.relayId);
}
}
@Override
protected void sendStopCommand(@Nullable Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
if (relay != null) {
client.stopRelay(relay.relayId);
}
}
@Override
protected void sendRunAllCommand()
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.runAllRelays(controllerId);
}
@Override
protected void sendRunAllCommand(int seconds)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.runAllRelays(seconds, controllerId);
}
@Override
protected void sendStopAllCommand()
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.stopAllRelays(controllerId);
}
private void updateSensors(StatusScheduleResponse status) {
status.sensors.forEach(sensor -> {
String group = "sensor" + sensor.input;
updateGroupState(group, CHANNEL_SENSOR_MODE, new DecimalType(sensor.type));
updateGroupState(group, CHANNEL_SENSOR_NAME, new StringType(sensor.name));
updateGroupState(group, CHANNEL_SENSOR_OFFTIMER, new DecimalType(sensor.offtimer));
updateGroupState(group, CHANNEL_SENSOR_TIMER, new DecimalType(sensor.timer));
// Some fields are missing depending on sensor type.
if (sensor.offlevel != null) {
updateGroupState(group, CHANNEL_SENSOR_OFFLEVEL, new DecimalType(sensor.offlevel));
}
if (sensor.active != null) {
updateGroupState(group, CHANNEL_SENSOR_ACTIVE, sensor.active > 0 ? OnOffType.ON : OnOffType.OFF);
}
});
}
private void updateForecast(StatusScheduleResponse status) {
int i = 1;
for (Forecast forecast : status.forecast) {
String group = "forecast" + (i++);
updateGroupState(group, CHANNEL_FORECAST_CONDITIONS, new StringType(forecast.conditions));
updateGroupState(group, CHANNEL_FORECAST_DAY, new StringType(forecast.day));
updateGroupState(group, CHANNEL_FORECAST_HUMIDITY, new DecimalType(forecast.humidity));
updateTemperature(forecast.tempHi, group, CHANNEL_FORECAST_TEMPERATURE_HIGH);
updateTemperature(forecast.tempLo, group, CHANNEL_FORECAST_TEMPERATURE_LOW);
updateWindspeed(forecast.wind, group, CHANNEL_FORECAST_WIND);
}
}
private void updateTemperature(String tempString, String group, String channel) {
Matcher matcher = TEMPERATURE_PATTERN.matcher(tempString);
if (matcher.matches()) {
try {
updateGroupState(group, channel, new QuantityType<>(Double.valueOf(matcher.group(1)),
"C".equals(matcher.group(2)) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT));
} catch (NumberFormatException e) {
logger.debug("Could not parse temperature string {} ", tempString);
}
}
}
private void updateWindspeed(String windString, String group, String channel) {
Matcher matcher = WIND_SPEED_PATTERN.matcher(windString);
if (matcher.matches()) {
try {
updateGroupState(group, channel, new QuantityType<>(Integer.parseInt(matcher.group(1)),
"kph".equals(matcher.group(2)) ? SIUnits.KILOMETRE_PER_HOUR : ImperialUnits.MILES_PER_HOUR));
} catch (NumberFormatException e) {
logger.debug("Could not parse wind string {} ", windString);
}
}
}
private void updateControllerProperties(Controller controller) {
getThing().setProperty(PROPERTY_CONTROLLER_ID, String.valueOf(controller.controllerId));
getThing().setProperty(PROPERTY_NAME, controller.name);
getThing().setProperty(PROPERTY_DESCRIPTION, controller.description);
getThing().setProperty(PROPERTY_LOCATION, controller.latitude + "," + controller.longitude);
getThing().setProperty(PROPERTY_ADDRESS, controller.address);
}
private @Nullable Controller getController(int controllerId, List<Controller> controllers) {
Optional<@NonNull Controller> optionalController = controllers.stream()
.filter(c -> controllerId == c.controllerId.intValue()).findAny();
return optionalController.isPresent() ? optionalController.get() : null;
}
}

View File

@@ -0,0 +1,28 @@
/**
* 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.hydrawise.internal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Controller;
/**
*
* @author Dan Cunningham - Initial contribution
*
*/
@NonNullByDefault
public interface HydrawiseControllerListener {
public void onData(List<Controller> controllers);
}

View File

@@ -15,13 +15,16 @@ package org.openhab.binding.hydrawise.internal;
import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.hydrawise.internal.handler.HydrawiseAccountHandler;
import org.openhab.binding.hydrawise.internal.handler.HydrawiseControllerHandler;
import org.openhab.binding.hydrawise.internal.handler.HydrawiseLocalHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.io.net.http.HttpClientFactory;
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;
@@ -40,14 +43,15 @@ import org.osgi.service.component.annotations.Reference;
@NonNullByDefault
@Component(configurationPid = "binding.hydrawise", service = ThingHandlerFactory.class)
public class HydrawiseHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream.of(THING_TYPE_CLOUD, THING_TYPE_LOCAL)
.collect(Collectors.toSet());
private final HttpClient httpClient;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
THING_TYPE_CONTROLLER, THING_TYPE_LOCAL);
private HttpClient httpClient;
private OAuthFactory oAuthFactory;
@Activate
public HydrawiseHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
public HydrawiseHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference OAuthFactory oAuthFactory) {
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@@ -60,8 +64,12 @@ public class HydrawiseHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CLOUD.equals(thingTypeUID)) {
return new HydrawiseCloudHandler(thing, httpClient);
if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
return new HydrawiseAccountHandler((Bridge) thing, httpClient, oAuthFactory);
}
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
return new HydrawiseControllerHandler(thing);
}
if (THING_TYPE_LOCAL.equals(thingTypeUID)) {

View File

@@ -1,91 +0,0 @@
/**
* 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.hydrawise.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseLocalApiClient;
import org.openhab.binding.hydrawise.internal.api.model.Relay;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HydrawiseLocalHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class HydrawiseLocalHandler extends HydrawiseHandler {
private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalHandler.class);
HydrawiseLocalApiClient client;
public HydrawiseLocalHandler(Thing thing, HttpClient httpClient) {
super(thing);
client = new HydrawiseLocalApiClient(httpClient);
}
@Override
protected void configure() throws HydrawiseConnectionException, HydrawiseAuthenticationException {
HydrawiseLocalConfiguration configuration = getConfig().as(HydrawiseLocalConfiguration.class);
this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS);
logger.trace("Connecting to host {}", configuration.host);
client.setCredentials(configuration.host, configuration.username, configuration.password);
pollController();
}
@Override
protected void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException {
updateZones(client.getLocalSchedule());
}
@Override
protected void sendRunCommand(int seconds, Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.runRelay(seconds, relay.relay);
}
@Override
protected void sendRunCommand(Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.runRelay(relay.relay);
}
@Override
protected void sendStopCommand(Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.stopRelay(relay.relay);
}
@Override
protected void sendRunAllCommand()
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.runAllRelays();
}
@Override
protected void sendRunAllCommand(int seconds)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.runAllRelays(seconds);
}
@Override
protected void sendStopAllCommand()
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException {
client.stopAllRelays();
}
}

View File

@@ -12,12 +12,23 @@
*/
package org.openhab.binding.hydrawise.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Thrown when the Hydrawise cloud or local API returns back a "unauthorized" response to commands
*
* Thrown when the Hydrawise API returns back a "unauthorized" response to commands
*
* @author Dan Cunningham - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class HydrawiseAuthenticationException extends Exception {
private static final long serialVersionUID = 1L;
public HydrawiseAuthenticationException() {
super();
}
public HydrawiseAuthenticationException(@Nullable String message) {
super(message);
}
}

View File

@@ -1,312 +0,0 @@
/**
* 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.hydrawise.internal.api;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.hydrawise.internal.api.model.CustomerDetailsResponse;
import org.openhab.binding.hydrawise.internal.api.model.Response;
import org.openhab.binding.hydrawise.internal.api.model.SetControllerResponse;
import org.openhab.binding.hydrawise.internal.api.model.SetZoneResponse;
import org.openhab.binding.hydrawise.internal.api.model.StatusScheduleResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link HydrawiseCloudApiClient} communicates with the cloud based Hydrawise API service
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class HydrawiseCloudApiClient {
private final Logger logger = LoggerFactory.getLogger(HydrawiseCloudApiClient.class);
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
private static final String BASE_URL = "https://app.hydrawise.com/api/v1/";
private static final String STATUS_SCHEDUE_URL = BASE_URL
+ "statusschedule.php?api_key=%s&controller_id=%d&hours=168";
private static final String CUSTOMER_DETAILS_URL = BASE_URL + "customerdetails.php?api_key=%s&type=controllers";
private static final String SET_CONTROLLER_URL = BASE_URL
+ "setcontroller.php?api_key=%s&controller_id=%d&json=true";
private static final String SET_ZONE_URL = BASE_URL + "setzone.php?period_id=999";
private static final int TIMEOUT_SECONDS = 30;
private final HttpClient httpClient;
private String apiKey;
/**
* Initializes the API client with a HydraWise API key from a user's account and the HTTPClient to use
*
*/
public HydrawiseCloudApiClient(String apiKey, HttpClient httpClient) {
this.apiKey = apiKey;
this.httpClient = httpClient;
}
/**
* Initializes the API client with a HTTPClient to use
*
*/
public HydrawiseCloudApiClient(HttpClient httpClient) {
this("", httpClient);
}
/**
* Set a new API key to use for requests
*
* @param apiKey
*/
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
/**
* Retrieves the {@link StatusScheduleResponse} for a given controller
*
* @param controllerId
* @return
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
*/
public StatusScheduleResponse getStatusSchedule(int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
String json = doGet(String.format(STATUS_SCHEDUE_URL, apiKey, controllerId));
StatusScheduleResponse response = Objects.requireNonNull(gson.fromJson(json, StatusScheduleResponse.class));
throwExceptionIfResponseError(response);
return response;
}
/***
* Retrieves the {@link CustomerDetailsResponse}
*
* @return
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
*/
public CustomerDetailsResponse getCustomerDetails()
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
String json = doGet(String.format(CUSTOMER_DETAILS_URL, apiKey));
CustomerDetailsResponse response = Objects.requireNonNull(gson.fromJson(json, CustomerDetailsResponse.class));
throwExceptionIfResponseError(response);
return response;
}
/***
* Sets the controller with supplied {@param id} as the current controller
*
* @param id
* @return SetControllerResponse
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public SetControllerResponse setController(int id)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
String json = doGet(String.format(SET_CONTROLLER_URL, apiKey, id));
SetControllerResponse response = Objects.requireNonNull(gson.fromJson(json, SetControllerResponse.class));
throwExceptionIfResponseError(response);
if (!response.message.equals("OK")) {
throw new HydrawiseCommandException(response.message);
}
return response;
}
/***
* Stops a given relay
*
* @param relayId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String stopRelay(int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(
new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("stop").relayId(relayId).toString());
}
/**
* Stops all relays on a given controller
*
* @param controllerId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String stopAllRelays(int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("stopall")
.controllerId(controllerId).toString());
}
/**
* Runs a relay for the default amount of time
*
* @param relayId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String runRelay(int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(
new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("run").relayId(relayId).toString());
}
/**
* Runs a relay for the given amount of seconds
*
* @param seconds
* @param relayId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String runRelay(int seconds, int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("run").relayId(relayId)
.duration(seconds).toString());
}
/**
* Run all relays on a given controller for the default amount of time
*
* @param controllerId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String runAllRelays(int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("runall")
.controllerId(controllerId).toString());
}
/***
* Run all relays on a given controller for the amount of seconds
*
* @param seconds
* @param controllerId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String runAllRelays(int seconds, int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("runall")
.controllerId(controllerId).duration(seconds).toString());
}
/**
* Suspends a given relay
*
* @param relayId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String suspendRelay(int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(
new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("suspend").relayId(relayId).toString());
}
/**
* Suspends a given relay for an amount of seconds
*
* @param seconds
* @param relayId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String suspendRelay(int seconds, int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("suspend").relayId(relayId)
.duration(seconds).toString());
}
/**
* Suspend all relays on a given controller for an amount of seconds
*
* @param seconds
* @param controllerId
* @return Response message
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public String suspendAllRelays(int seconds, int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
return relayCommand(new HydrawiseZoneCommandBuilder(SET_ZONE_URL, apiKey).action("suspendall")
.controllerId(controllerId).duration(seconds).toString());
}
private String relayCommand(String url)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
String json = doGet(url);
SetZoneResponse response = Objects.requireNonNull(gson.fromJson(json, SetZoneResponse.class));
throwExceptionIfResponseError(response);
if ("error".equals(response.messageType)) {
throw new HydrawiseCommandException(response.message);
}
return response.message;
}
private String doGet(String url) throws HydrawiseConnectionException {
logger.trace("Getting {}", url);
ContentResponse response;
try {
response = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.send();
} catch (Exception e) {
throw new HydrawiseConnectionException(e);
}
if (response.getStatus() != 200) {
throw new HydrawiseConnectionException(
"Could not connect to Hydrawise API. Response code " + response.getStatus());
}
String stringResponse = response.getContentAsString();
logger.trace("Response: {}", stringResponse);
return stringResponse;
}
private void throwExceptionIfResponseError(Response response)
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
String error = response.errorMsg;
if (error != null) {
if (error.equalsIgnoreCase("unauthorized")) {
throw new HydrawiseAuthenticationException();
} else {
throw new HydrawiseConnectionException(response.errorMsg);
}
}
}
}

View File

@@ -12,13 +12,17 @@
*/
package org.openhab.binding.hydrawise.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown when command responses return a error message
*
* @author Dan Cunningham - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class HydrawiseCommandException extends Exception {
private static final long serialVersionUID = 1L;
public HydrawiseCommandException(String message) {
super(message);
}

View File

@@ -12,13 +12,16 @@
*/
package org.openhab.binding.hydrawise.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown for connection issues to the Hydrawise controller
*
* @author Dan Cunningham - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class HydrawiseConnectionException extends Exception {
private static final long serialVersionUID = 1L;
public HydrawiseConnectionException(Exception e) {
super(e);

View File

@@ -0,0 +1,341 @@
/**
* 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.hydrawise.internal.api.graphql;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
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.Response;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.ControllerStatus;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Forecast;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Mutation;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse.MutationResponseStatus;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse.StatusCode;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryRequest;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryResponse;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.ScheduledRuns;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Sensor;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Zone;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.ZoneRun;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
*
* @author Dan Cunningham - Initial contribution
*
*/
@NonNullByDefault
public class HydrawiseGraphQLClient {
private final Logger logger = LoggerFactory.getLogger(HydrawiseGraphQLClient.class);
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Zone.class, new ResponseDeserializer<Zone>())
.registerTypeAdapter(ScheduledRuns.class, new ResponseDeserializer<ScheduledRuns>())
.registerTypeAdapter(ZoneRun.class, new ResponseDeserializer<ZoneRun>())
.registerTypeAdapter(Forecast.class, new ResponseDeserializer<Forecast>())
.registerTypeAdapter(Sensor.class, new ResponseDeserializer<Forecast>())
.registerTypeAdapter(ControllerStatus.class, new ResponseDeserializer<ControllerStatus>()).create();
private static final String GRAPH_URL = "https://app.hydrawise.com/api/v2/graph";
private static final String MUTATION_START_ZONE = "startZone(zoneId: %d) { status }";
private static final String MUTATION_START_ZONE_CUSTOM = "startZone(zoneId: %d, customRunDuration: %d) { status }";
private static final String MUTATION_START_ALL_ZONES = "startAllZones(controllerId: %d){ status }";
private static final String MUTATION_START_ALL_ZONES_CUSTOM = "startAllZones(controllerId: %d, markRunAsScheduled: false, customRunDuration: %d ){ status }";
private static final String MUTATION_STOP_ZONE = "stopZone(zoneId: %d) { status }";
private static final String MUTATION_STOP_ALL_ZONES = "stopAllZones(controllerId: %d){ status }";
private static final String MUTATION_SUSPEND_ZONE = "suspendZone(zoneId: %d, until: \"%s\"){ status }";
private static final String MUTATION_SUSPEND_ALL_ZONES = "suspendAllZones(controllerId: %d, until: \"%s\"){ status }";
private static final String MUTATION_RESUME_ZONE = "resumeZone(zoneId: %d){ status }";
private static final String MUTATION_RESUME_ALL_ZONES = "resumeAllZones(controllerId: %d){ status }";
private final HttpClient httpClient;
private final OAuthClientService oAuthService;
private String queryString = "";
public HydrawiseGraphQLClient(HttpClient httpClient, OAuthClientService oAuthService) {
this.httpClient = httpClient;
this.oAuthService = oAuthService;
}
/**
* Sends a GrapQL query for controller data
*
* @return QueryResponse
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
*/
public @Nullable QueryResponse queryControllers()
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
QueryRequest query;
try {
query = new QueryRequest(getQueryString());
} catch (IOException e) {
throw new HydrawiseConnectionException(e);
}
String queryJson = gson.toJson(query);
String response = sendGraphQLQuery(queryJson);
return gson.fromJson(response, QueryResponse.class);
}
/***
* Stops a given relay
*
* @param relayId
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void stopRelay(int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_STOP_ZONE, relayId));
}
/**
* Stops all relays on a given controller
*
* @param controllerId
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void stopAllRelays(int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_STOP_ALL_ZONES, controllerId));
}
/**
* Runs a relay for the default amount of time
*
* @param relayId
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void runRelay(int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_START_ZONE, relayId));
}
/**
* Runs a relay for the given amount of seconds
*
* @param relayId
* @param seconds
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void runRelay(int relayId, int seconds)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_START_ZONE_CUSTOM, relayId, seconds));
}
/**
* Run all relays on a given controller for the default amount of time
*
* @param controllerId
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void runAllRelays(int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_START_ALL_ZONES, controllerId));
}
/***
* Run all relays on a given controller for the amount of seconds
*
* @param controllerId
* @param seconds
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void runAllRelays(int controllerId, int seconds)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_START_ALL_ZONES_CUSTOM, controllerId, seconds));
}
/**
* Suspends a given relay
*
* @param relayId
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void suspendRelay(int relayId, String until)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_SUSPEND_ZONE, relayId, until));
}
/**
* Resumes a given relay
*
* @param relayId
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void resumeRelay(int relayId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_RESUME_ZONE, relayId));
}
/**
* Suspend all relays on a given controller for an amount of seconds
*
* @param controllerId
* @param until
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void suspendAllRelays(int controllerId, String until)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_SUSPEND_ALL_ZONES, controllerId, until));
}
/**
* Resumes all relays on a given controller
*
* @param controllerId
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
* @throws HydrawiseCommandException
*/
public void resumeAllRelays(int controllerId)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
sendGraphQLMutation(String.format(MUTATION_RESUME_ALL_ZONES, controllerId));
}
private String sendGraphQLQuery(String content)
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
return sendGraphQLRequest(content);
}
private void sendGraphQLMutation(String content)
throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
Mutation mutation = new Mutation(content);
logger.debug("Sending Mutation {}", gson.toJson(mutation).toString());
String response = sendGraphQLRequest(gson.toJson(mutation).toString());
logger.debug("Mutation response {}", response);
MutationResponse mResponse = gson.fromJson(response, MutationResponse.class);
if (mResponse == null) {
throw new HydrawiseCommandException("Malformed response: " + response);
}
Optional<MutationResponseStatus> status = mResponse.data.values().stream().findFirst();
if (!status.isPresent()) {
throw new HydrawiseCommandException("Unknown response: " + response);
}
if (status.get().status != StatusCode.OK) {
throw new HydrawiseCommandException("Command Status: " + status.get().status.name());
}
}
private String sendGraphQLRequest(String content)
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
logger.trace("Sending Request: {}", content);
ContentResponse response;
final AtomicInteger responseCode = new AtomicInteger(0);
final StringBuilder responseMessage = new StringBuilder();
try {
AccessTokenResponse token = oAuthService.getAccessTokenResponse();
if (token == null) {
throw new HydrawiseAuthenticationException("Login required");
}
response = httpClient.newRequest(GRAPH_URL).method(HttpMethod.POST)
.content(new StringContentProvider(content), "application/json")
.header("Authorization", token.getTokenType() + " " + token.getAccessToken())
.onResponseFailure(new Response.FailureListener() {
@Override
public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
int status = response != null ? response.getStatus() : -1;
String reason = response != null ? response.getReason() : "Null response";
logger.trace("onFailure code: {} message: {}", status, reason);
responseCode.set(status);
responseMessage.append(reason);
}
}).send();
String stringResponse = response.getContentAsString();
logger.trace("Received Response: {}", stringResponse);
return stringResponse;
} catch (InterruptedException | TimeoutException | OAuthException | IOException e) {
logger.debug("Could not send request", e);
throw new HydrawiseConnectionException(e);
} catch (OAuthResponseException e) {
throw new HydrawiseAuthenticationException(e.getMessage());
} catch (ExecutionException e) {
// Hydrawise returns back a 40x status, but without a valid Realm , so jetty throws an exception,
// this allows us to catch this in a callback and handle accordingly
switch (responseCode.get()) {
case 401:
case 403:
throw new HydrawiseAuthenticationException(responseMessage.toString());
default:
throw new HydrawiseConnectionException(e);
}
}
}
private String getQueryString() throws IOException {
if (queryString.isBlank()) {
try (InputStream inputStream = HydrawiseGraphQLClient.class.getClassLoader()
.getResourceAsStream("query.graphql");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
queryString = bufferedReader.lines().collect(Collectors.joining("\n"));
}
}
return queryString;
}
class ResponseDeserializer<T> implements JsonDeserializer<T> {
@Override
@Nullable
public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
return new Gson().fromJson(je, type);
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class AuthToken {
public String tokenType;
public Integer expiresIn;
public String accessToken;
public String refreshToken;
public Long issued;
public AuthToken() {
super();
issued = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
import java.util.List;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Controller {
public Integer id;
public String name;
public ControllerStatus status;
public Location location;
public List<Zone> zones = null;
public List<Sensor> sensors = null;
public List<Forecast> forecast = null;
}

View File

@@ -0,0 +1,26 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
*
* @author Dan Cunningham - Initial contribution
*
*/
public class ControllerStatus {
public Integer id;
public String name;
public String summary;
public Boolean online;
public Time lastContact;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Coordinates {
public Double latitude;
public Double longitude;
}

View File

@@ -0,0 +1,24 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
import java.util.List;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Customer {
public String email;
public String lastContact;
public List<Controller> controllers = null;
}

View File

@@ -0,0 +1,20 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Data {
public Customer me;
}

View File

@@ -0,0 +1,29 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Forecast {
public String time;
public String updateTime;
public String conditions;
public UnitValue highTemperature;
public UnitValue lowTemperature;
public UnitValue evapotranspiration;
public Integer probabilityOfPrecipitation;
public UnitValue precipitation;
public Number averageHumidity;
public UnitValue averageWindSpeed;
}

View File

@@ -0,0 +1,22 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Icon {
public Integer id;
public String fileName;
public Object customImage;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Input {
public Integer number;
public String label;
}

View File

@@ -0,0 +1,23 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
import java.util.List;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Location {
public Coordinates coordinates;
public List<Forecast> forecast;
}

View File

@@ -0,0 +1,26 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Mutation {
private static final String MUTATION_TEMPLATE = "mutation { %s }";
public String query;
public Mutation(String graphQLquery) {
this.query = String.format(MUTATION_TEMPLATE, graphQLquery);
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
import java.util.Map;
/**
* @author Dan Cunningham - Initial contribution
*/
public class MutationResponse {
public Map<String, MutationResponseStatus> data;
public class MutationResponseStatus {
public StatusCode status;
}
public enum StatusCode {
OK,
WARNING,
ERROR;
}
}

View File

@@ -0,0 +1,20 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class PastRuns {
public ZoneRun lastRun;
}

View File

@@ -0,0 +1,24 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class QueryRequest {
public String query;
public QueryRequest(String query) {
this.query = query;
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
import java.util.List;
/**
* @author Dan Cunningham - Initial contribution
*/
public class QueryResponse {
public Data data;
public List<QueryResponseError> errors;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class QueryResponseError {
public String message;
public QueryResponseErrorExtensions extentions;
}

View File

@@ -0,0 +1,20 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class QueryResponseErrorExtensions {
public String category;
}

View File

@@ -0,0 +1,22 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class ScheduledRuns {
public String summary;
public ZoneRun nextRun;
public ZoneRun currentRun;
}

View File

@@ -0,0 +1,24 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Sensor {
public Integer id;
public String name;
public Input input;
public SensorStatus status;
public SensorModel model;
}

View File

@@ -0,0 +1,24 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class SensorModel {
public String modeType;
public Boolean active;
public Integer offLevel;
public Integer offTimer;
public Integer delay;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class SensorStatus {
public Boolean active;
public UnitValue waterFlow;
}

View File

@@ -0,0 +1,20 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Time {
public Integer timestamp;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class UnitValue {
public Number value;
public String unit;
}

View File

@@ -0,0 +1,26 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class Zone {
public Integer id;
public String name;
public ZoneStatus status;
public Icon icon;
public ZoneNumber number;
public ScheduledRuns scheduledRuns;
public PastRuns pastRuns;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class ZoneNumber {
public Integer value;
public String label;
}

View File

@@ -0,0 +1,23 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class ZoneRun {
public String id;
public Time startTime;
public Time endTime;
public Integer duration;
}

View File

@@ -0,0 +1,20 @@
/**
* 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.hydrawise.internal.api.graphql.dto;
/**
* @author Dan Cunningham - Initial contribution
*/
public class ZoneStatus {
public Time suspendedUntil;
}

View File

@@ -10,22 +10,25 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api;
package org.openhab.binding.hydrawise.internal.api.local;
import java.net.URI;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
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.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
import org.openhab.binding.hydrawise.internal.api.model.SetZoneResponse;
import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse;
import org.openhab.binding.hydrawise.internal.api.local.dto.SetZoneResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -92,11 +95,12 @@ public class HydrawiseLocalApiClient {
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
*/
@Nullable
public LocalScheduleResponse getLocalSchedule()
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
String json = doGet(localGetURL);
LocalScheduleResponse response = gson.fromJson(json, LocalScheduleResponse.class);
return Objects.requireNonNull(response);
return response;
}
/**

View File

@@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api;
package org.openhab.binding.hydrawise.internal.api.local;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HydrawiseZoneCommandBuilder} class builds a command URL string to use when sending commands to the
@@ -19,6 +21,7 @@ package org.openhab.binding.hydrawise.internal.api;
* @author Dan Cunningham - Initial contribution
*
*/
@NonNullByDefault
class HydrawiseZoneCommandBuilder {
private final StringBuilder builder;

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;
@@ -59,7 +59,5 @@ public class Controller {
public String statusIcon;
public Boolean online;
public List<String> tags = null;
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Forecast} class models a daily weather forecast

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.LinkedList;
import java.util.List;
@@ -22,9 +22,9 @@ import java.util.List;
*/
public class LocalScheduleResponse extends Response {
public List<Running> running = new LinkedList<>();
public List<Running> running = new LinkedList<Running>();
public List<Relay> relays = new LinkedList<>();
public List<Relay> relays = new LinkedList<Relay>();
public String name;

View File

@@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PlanArray} class models am account plan.
@@ -63,7 +65,8 @@ public class PlanArray {
public String filetypeall;
public String plan_type;
@SerializedName(value = "plan_type")
public String planType;
public String pushNotification;

View File

@@ -10,9 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
import com.google.gson.annotations.SerializedName;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Relay} class models the Relay response message
@@ -23,27 +21,19 @@ public class Relay {
public Integer relayId;
public Integer relay;
public String name;
public String icon;
public String lastwater;
public Integer time;
public Integer type;
@SerializedName("run")
public String runTime;
public Integer relay;
@SerializedName("run_seconds")
public Integer runTimeSeconds;
public String name;
public String nicetime;
public Integer frequency;
public String id;
public String timestr;
public Integer runSeconds;
/**
* Returns back the actual relay number when multiple controllers are chained.

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Response} class models Response messages

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link Running} class models a running relay

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.List;

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link SetControllerResponse} class models the SetController response message

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
/**
* The {@link SetZoneResponse} class models the SetZone response message

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal.api.model;
package org.openhab.binding.hydrawise.internal.api.local.dto;
import java.util.LinkedList;
import java.util.List;
@@ -30,7 +30,7 @@ public class StatusScheduleResponse extends LocalScheduleResponse {
public Integer nextpoll;
public List<Sensor> sensors = new LinkedList<>();
public List<Sensor> sensors = new LinkedList<Sensor>();
public String message;
@@ -52,7 +52,7 @@ public class StatusScheduleResponse extends LocalScheduleResponse {
public String lastContact;
public List<Forecast> forecast = new LinkedList<>();
public List<Forecast> forecast = new LinkedList<Forecast>();
public String status;

View File

@@ -0,0 +1,28 @@
/**
* 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.hydrawise.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HydrawiseAccountConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class HydrawiseAccountConfiguration {
public String userName = "";
public String password = "";
public Boolean savePassword = false;
public Integer refreshInterval = 60;
}

View File

@@ -10,27 +10,19 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal;
package org.openhab.binding.hydrawise.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HydrawiseCloudConfiguration} class contains fields mapping thing configuration parameters.
* The {@link HydrawiseControllerConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Dan Cunningham - Initial contribution
*/
public class HydrawiseCloudConfiguration {
/**
* Customer API key {@link https://app.hydrawise.com/config/account}
*/
public String apiKey;
/**
* refresh interval in seconds.
*/
public Integer refresh;
@NonNullByDefault
public class HydrawiseControllerConfiguration {
/**
* optional id of the controller to connect to
*/
public Integer controllerId;
public Integer controllerId = -1;
}

View File

@@ -10,30 +10,31 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal;
package org.openhab.binding.hydrawise.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HydrawiseLocalConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class HydrawiseLocalConfiguration {
/**
* Host or IP for local controller
*/
public String host;
public String host = "";
/**
* User name (admin) for local controller
*/
public String username;
public String username = "";
/**
* Password for local controller
*/
public String password;
public String password = "";
/**
* refresh interval in seconds.
*/
public int refresh;
public int refresh = 30;
}

View File

@@ -0,0 +1,110 @@
/**
* 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.hydrawise.internal.discovery;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants;
import org.openhab.binding.hydrawise.internal.HydrawiseControllerListener;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Controller;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Customer;
import org.openhab.binding.hydrawise.internal.handler.HydrawiseAccountHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.osgi.service.component.annotations.Component;
/**
*
* @author Dan Cunningham - Initial contribution
*
*/
@NonNullByDefault
@Component(service = ThingHandlerService.class)
public class HydrawiseCloudControllerDiscoveryService extends AbstractDiscoveryService
implements HydrawiseControllerListener, ThingHandlerService {
private static final int TIMEOUT = 5;
@Nullable
HydrawiseAccountHandler handler;
public HydrawiseCloudControllerDiscoveryService() {
super(Collections.singleton(HydrawiseBindingConstants.THING_TYPE_CONTROLLER), TIMEOUT, true);
}
@Override
protected void startScan() {
HydrawiseAccountHandler localHandler = this.handler;
if (localHandler != null) {
Customer data = localHandler.lastData();
if (data != null) {
data.controllers.forEach(controller -> addDiscoveryResults(controller));
}
}
}
@Override
public void deactivate() {
HydrawiseAccountHandler localHandler = this.handler;
if (localHandler != null) {
removeOlderResults(new Date().getTime(), localHandler.getThing().getUID());
}
}
@Override
protected synchronized void stopScan() {
super.stopScan();
HydrawiseAccountHandler localHandler = this.handler;
if (localHandler != null) {
removeOlderResults(getTimestampOfLastScan(), localHandler.getThing().getUID());
}
}
@Override
public void onData(List<Controller> controllers) {
controllers.forEach(controller -> addDiscoveryResults(controller));
}
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (HydrawiseAccountHandler) handler;
this.handler.addControllerListeners(this);
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
private void addDiscoveryResults(Controller controller) {
HydrawiseAccountHandler localHandler = this.handler;
if (localHandler != null) {
String label = String.format("Hydrawise Controller %s", controller.name);
int id = controller.id;
ThingUID bridgeUID = localHandler.getThing().getUID();
ThingUID thingUID = new ThingUID(HydrawiseBindingConstants.THING_TYPE_CONTROLLER, bridgeUID,
String.valueOf(id));
thingDiscovered(DiscoveryResultBuilder.create(thingUID).withLabel(label).withBridge(bridgeUID)
.withProperty(HydrawiseBindingConstants.CONFIG_CONTROLLER_ID, id)
.withRepresentationProperty(String.valueOf(HydrawiseBindingConstants.CONFIG_CONTROLLER_ID))
.build());
}
}
}

View File

@@ -0,0 +1,216 @@
/**
* 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.hydrawise.internal.handler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.hydrawise.internal.HydrawiseControllerListener;
import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
import org.openhab.binding.hydrawise.internal.api.graphql.HydrawiseGraphQLClient;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Customer;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryResponse;
import org.openhab.binding.hydrawise.internal.config.HydrawiseAccountConfiguration;
import org.openhab.binding.hydrawise.internal.discovery.HydrawiseCloudControllerDiscoveryService;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HydrawiseAccountHandler} is responsible for handling for connecting to a Hydrawise account and polling for
* controller data
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class HydrawiseAccountHandler extends BaseBridgeHandler implements AccessTokenRefreshListener {
private final Logger logger = LoggerFactory.getLogger(HydrawiseAccountHandler.class);
/**
* Minimum amount of time we can poll for updates
*/
private static final int MIN_REFRESH_SECONDS = 30;
private static final String BASE_URL = "https://app.hydrawise.com/api/v2/";
private static final String AUTH_URL = BASE_URL + "oauth/access-token";
private static final String CLIENT_SECRET = "zn3CrjglwNV1";
private static final String CLIENT_ID = "hydrawise_app";
private static final String SCOPE = "all";
private final List<HydrawiseControllerListener> controllerListeners = new ArrayList<HydrawiseControllerListener>();
private final HydrawiseGraphQLClient apiClient;
private final OAuthClientService oAuthService;
private @Nullable ScheduledFuture<?> pollFuture;
private @Nullable Customer lastData;
private int refresh;
public HydrawiseAccountHandler(final Bridge bridge, final HttpClient httpClient, final OAuthFactory oAuthFactory) {
super(bridge);
this.oAuthService = oAuthFactory.createOAuthClientService(getThing().toString(), AUTH_URL, AUTH_URL, CLIENT_ID,
CLIENT_SECRET, SCOPE, false);
oAuthService.addAccessTokenRefreshListener(this);
this.apiClient = new HydrawiseGraphQLClient(httpClient, oAuthService);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
logger.debug("Handler initialized.");
scheduler.schedule(this::configure, 0, TimeUnit.SECONDS);
}
@Override
public void dispose() {
logger.debug("Handler disposed.");
clearPolling();
}
@Override
public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
logger.debug("Auth Token Refreshed, expires in {}", tokenResponse.getExpiresIn());
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(HydrawiseCloudControllerDiscoveryService.class);
}
public void addControllerListeners(HydrawiseControllerListener listener) {
this.controllerListeners.add(listener);
Customer data = lastData;
if (data != null) {
listener.onData(data.controllers);
}
}
public void removeControllerListeners(HydrawiseControllerListener listener) {
this.controllerListeners.remove(listener);
}
public @Nullable HydrawiseGraphQLClient graphQLClient() {
return apiClient;
}
public @Nullable Customer lastData() {
return lastData;
}
public void refreshData(int delaySeconds) {
initPolling(delaySeconds, this.refresh);
}
private void configure() {
HydrawiseAccountConfiguration config = getConfig().as(HydrawiseAccountConfiguration.class);
try {
if (!config.userName.isEmpty() && !config.password.isEmpty()) {
if (!config.savePassword) {
Configuration editedConfig = editConfiguration();
editedConfig.remove("password");
updateConfiguration(editedConfig);
}
oAuthService.getAccessTokenByResourceOwnerPasswordCredentials(config.userName, config.password, SCOPE);
} else if (oAuthService.getAccessTokenResponse() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login credentials required.");
return;
}
this.refresh = Math.max(config.refreshInterval, MIN_REFRESH_SECONDS);
initPolling(0, refresh);
} catch (OAuthException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (OAuthResponseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login credentials required.");
}
}
/**
* Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
* and we need to poll sooner then the next refresh cycle.
*/
private synchronized void initPolling(int initalDelay, int refresh) {
clearPolling();
pollFuture = scheduler.scheduleWithFixedDelay(this::poll, initalDelay, refresh, TimeUnit.SECONDS);
}
/**
* Stops/clears this thing's polling future
*/
private void clearPolling() {
ScheduledFuture<?> localFuture = pollFuture;
if (isFutureValid(localFuture)) {
if (localFuture != null) {
localFuture.cancel(false);
}
}
}
private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
return future != null && !future.isCancelled();
}
private void poll() {
poll(true);
}
private void poll(boolean retry) {
try {
QueryResponse response = apiClient.queryControllers();
if (response == null) {
throw new HydrawiseConnectionException("Malformed response");
}
if (response.errors != null && response.errors.size() > 0) {
throw new HydrawiseConnectionException(response.errors.stream().map(error -> error.message).reduce("",
(messages, message) -> messages + message + ". "));
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
lastData = response.data.me;
controllerListeners.forEach(listener -> {
listener.onData(response.data.me.controllers);
});
} catch (HydrawiseConnectionException e) {
if (retry) {
logger.debug("Retrying failed poll", e);
poll(false);
} else {
logger.debug("Will try again during next poll period", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} catch (HydrawiseAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
clearPolling();
}
}
}

View File

@@ -0,0 +1,436 @@
/**
* 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.hydrawise.internal.handler;
import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import javax.measure.quantity.Speed;
import javax.measure.quantity.Temperature;
import javax.measure.quantity.Volume;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hydrawise.internal.HydrawiseControllerListener;
import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
import org.openhab.binding.hydrawise.internal.api.graphql.HydrawiseGraphQLClient;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Controller;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Forecast;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Sensor;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.UnitValue;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.Zone;
import org.openhab.binding.hydrawise.internal.api.graphql.dto.ZoneRun;
import org.openhab.binding.hydrawise.internal.config.HydrawiseControllerConfiguration;
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.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
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.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HydrawiseControllerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class HydrawiseControllerHandler extends BaseThingHandler implements HydrawiseControllerListener {
private final Logger logger = LoggerFactory.getLogger(HydrawiseControllerHandler.class);
private static final int DEFAULT_SUSPEND_TIME_HOURS = 24;
private static final int DEFAULT_REFRESH_SECONDS = 15;
// All responses use US local time formats
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM uu HH:mm:ss Z",
Locale.US);
private final Map<String, @Nullable State> stateMap = Collections
.synchronizedMap(new HashMap<String, @Nullable State>());
private final Map<String, @Nullable Zone> zoneMaps = Collections
.synchronizedMap(new HashMap<String, @Nullable Zone>());
private int controllerId;
public HydrawiseControllerHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
HydrawiseControllerConfiguration config = getConfigAs(HydrawiseControllerConfiguration.class);
controllerId = config.controllerId;
Bridge bridge = getBridge();
if (bridge != null) {
HydrawiseAccountHandler handler = (HydrawiseAccountHandler) bridge.getHandler();
if (handler != null) {
handler.addControllerListeners(this);
if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("handleCommand channel {} Command {}", channelUID.getAsString(), command.toFullString());
if (getThing().getStatus() != ThingStatus.ONLINE) {
logger.debug("Controller is NOT ONLINE and is not responding to commands");
return;
}
// remove our cached state for this, will be safely updated on next poll
stateMap.remove(channelUID.getAsString());
if (command instanceof RefreshType) {
// we already removed this from the cache
return;
}
HydrawiseGraphQLClient client = apiClient();
if (client == null) {
logger.debug("API client not found");
return;
}
String group = channelUID.getGroupId();
String channelId = channelUID.getIdWithoutGroup();
boolean allCommand = CHANNEL_GROUP_ALLZONES.equals(group);
Zone zone = zoneMaps.get(group);
if (!allCommand && zone == null) {
logger.debug("Zone not found {}", group);
return;
}
try {
switch (channelId) {
case CHANNEL_ZONE_RUN_CUSTOM:
if (!(command instanceof QuantityType<?>)) {
logger.warn("Invalid command type for run custom {}", command.getClass().getName());
return;
}
QuantityType<?> time = ((QuantityType<?>) command).toUnit(Units.SECOND);
if (time == null) {
return;
}
if (allCommand) {
client.runAllRelays(controllerId, time.intValue());
} else if (zone != null) {
client.runRelay(zone.id, time.intValue());
}
break;
case CHANNEL_ZONE_RUN:
if (!(command instanceof OnOffType)) {
logger.warn("Invalid command type for run {}", command.getClass().getName());
return;
}
if (allCommand) {
if (command == OnOffType.ON) {
client.runAllRelays(controllerId);
} else {
client.stopAllRelays(controllerId);
}
} else if (zone != null) {
if (command == OnOffType.ON) {
client.runRelay(zone.id);
} else {
client.stopRelay(zone.id);
}
}
break;
case CHANNEL_ZONE_SUSPEND:
if (!(command instanceof OnOffType)) {
logger.warn("Invalid command type for suspend {}", command.getClass().getName());
return;
}
if (allCommand) {
if (command == OnOffType.ON) {
client.suspendAllRelays(controllerId, OffsetDateTime.now(ZoneOffset.UTC)
.plus(DEFAULT_SUSPEND_TIME_HOURS, ChronoUnit.HOURS).format(DATE_FORMATTER));
} else {
client.resumeAllRelays(controllerId);
}
} else if (zone != null) {
if (command == OnOffType.ON) {
client.suspendRelay(zone.id, OffsetDateTime.now(ZoneOffset.UTC)
.plus(DEFAULT_SUSPEND_TIME_HOURS, ChronoUnit.HOURS).format(DATE_FORMATTER));
} else {
client.resumeRelay(zone.id);
}
}
break;
case CHANNEL_ZONE_SUSPENDUNTIL:
if (!(command instanceof DateTimeType)) {
logger.warn("Invalid command type for suspend {}", command.getClass().getName());
return;
}
if (allCommand) {
client.suspendAllRelays(controllerId,
((DateTimeType) command).getZonedDateTime().format(DATE_FORMATTER));
} else if (zone != null) {
client.suspendRelay(zone.id,
((DateTimeType) command).getZonedDateTime().format(DATE_FORMATTER));
}
break;
default:
logger.warn("Uknown channelId {}", channelId);
return;
}
HydrawiseAccountHandler handler = getAccountHandler();
if (handler != null) {
handler.refreshData(DEFAULT_REFRESH_SECONDS);
}
} catch (HydrawiseCommandException | HydrawiseConnectionException e) {
logger.debug("Could not issue command", e);
} catch (HydrawiseAuthenticationException e) {
logger.debug("Credentials not valid");
}
}
@Override
public void onData(List<Controller> controllers) {
logger.trace("onData my controller id {}", controllerId);
controllers.stream().filter(c -> c.id == controllerId).findFirst().ifPresent(controller -> {
logger.trace("Updating Controller {} sensors {} forecast {} ", controller.id, controller.sensors,
controller.location.forecast);
updateController(controller);
if (controller.sensors != null) {
updateSensors(controller.sensors);
}
if (controller.location != null && controller.location.forecast != null) {
updateForecast(controller.location.forecast);
}
if (controller.zones != null) {
updateZones(controller.zones);
}
// update values with what the cloud tells us even though the controller may be offline
if (!controller.status.online) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Controller Offline: %s last seen %s", controller.status.summary,
secondsToDateTime(controller.status.lastContact.timestamp)));
} else if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
});
}
@Override
public void channelLinked(ChannelUID channelUID) {
// clear our cached value so the new channel gets updated on the next poll
stateMap.remove(channelUID.getId());
}
private void updateController(Controller controller) {
updateGroupState(CHANNEL_GROUP_CONTROLLER_SYSTEM, CHANNEL_CONTROLLER_NAME, new StringType(controller.name));
updateGroupState(CHANNEL_GROUP_CONTROLLER_SYSTEM, CHANNEL_CONTROLLER_SUMMARY,
new StringType(controller.status.summary));
updateGroupState(CHANNEL_GROUP_CONTROLLER_SYSTEM, CHANNEL_CONTROLLER_LAST_CONTACT,
secondsToDateTime(controller.status.lastContact.timestamp));
}
private void updateZones(List<Zone> zones) {
AtomicReference<Boolean> anyRunning = new AtomicReference<Boolean>(false);
AtomicReference<Boolean> anySuspended = new AtomicReference<Boolean>(false);
int i = 1;
for (Zone zone : zones) {
String group = "zone" + (i++);
zoneMaps.put(group, zone);
logger.trace("Updateing Zone {} {} ", group, zone.name);
updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(zone.name));
updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + zone.icon.fileName));
if (zone.scheduledRuns != null) {
updateGroupState(group, CHANNEL_ZONE_SUMMARY,
zone.scheduledRuns.summary != null ? new StringType(zone.scheduledRuns.summary)
: UnDefType.UNDEF);
ZoneRun nextRun = zone.scheduledRuns.nextRun;
if (nextRun != null) {
updateGroupState(group, CHANNEL_ZONE_DURATION, new QuantityType<>(nextRun.duration, Units.MINUTE));
updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME,
secondsToDateTime(nextRun.startTime.timestamp));
} else {
updateGroupState(group, CHANNEL_ZONE_DURATION, UnDefType.UNDEF);
updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
}
ZoneRun currRunn = zone.scheduledRuns.currentRun;
if (currRunn != null) {
updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(
currRunn.endTime.timestamp - Instant.now().getEpochSecond(), Units.SECOND));
anyRunning.set(true);
} else {
updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(0, Units.MINUTE));
}
}
if (zone.status.suspendedUntil != null) {
updateGroupState(group, CHANNEL_ZONE_SUSPEND, OnOffType.ON);
updateGroupState(group, CHANNEL_ZONE_SUSPENDUNTIL,
secondsToDateTime(zone.status.suspendedUntil.timestamp));
anySuspended.set(true);
} else {
updateGroupState(group, CHANNEL_ZONE_SUSPEND, OnOffType.OFF);
updateGroupState(group, CHANNEL_ZONE_SUSPENDUNTIL, UnDefType.UNDEF);
}
}
updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN, anyRunning.get() ? OnOffType.ON : OnOffType.OFF);
updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_SUSPEND,
anySuspended.get() ? OnOffType.ON : OnOffType.OFF);
updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_SUSPENDUNTIL, UnDefType.UNDEF);
}
private void updateSensors(List<Sensor> sensors) {
int i = 1;
for (Sensor sensor : sensors) {
String group = "sensor" + (i++);
updateGroupState(group, CHANNEL_SENSOR_NAME, new StringType(sensor.name));
if (sensor.model.offTimer != null) {
updateGroupState(group, CHANNEL_SENSOR_OFFTIMER,
new QuantityType<>(sensor.model.offTimer, Units.SECOND));
}
if (sensor.model.delay != null) {
updateGroupState(group, CHANNEL_SENSOR_DELAY, new QuantityType<>(sensor.model.delay, Units.SECOND));
}
if (sensor.model.offLevel != null) {
updateGroupState(group, CHANNEL_SENSOR_OFFLEVEL, new DecimalType(sensor.model.offLevel));
}
if (sensor.status.active != null) {
updateGroupState(group, CHANNEL_SENSOR_ACTIVE, sensor.status.active ? OnOffType.ON : OnOffType.OFF);
}
if (sensor.status.waterFlow != null) {
updateGroupState(group, CHANNEL_SENSOR_WATERFLOW,
waterFlowToQuantityType(sensor.status.waterFlow.value, sensor.status.waterFlow.unit));
}
}
}
private void updateForecast(List<Forecast> forecasts) {
int i = 1;
for (Forecast forecast : forecasts) {
String group = "forecast" + (i++);
updateGroupState(group, CHANNEL_FORECAST_TIME, stringToDateTime(forecast.time));
updateGroupState(group, CHANNEL_FORECAST_CONDITIONS, new StringType(forecast.conditions));
updateGroupState(group, CHANNEL_FORECAST_HUMIDITY, new DecimalType(forecast.averageHumidity.intValue()));
updateTemperature(forecast.highTemperature, group, CHANNEL_FORECAST_TEMPERATURE_HIGH);
updateTemperature(forecast.lowTemperature, group, CHANNEL_FORECAST_TEMPERATURE_LOW);
updateWindspeed(forecast.averageWindSpeed, group, CHANNEL_FORECAST_WIND);
// this seems to sometimes be optional
if (forecast.evapotranspiration != null) {
updateGroupState(group, CHANNEL_FORECAST_EVAPOTRANSPRIATION,
new DecimalType(forecast.evapotranspiration.value.floatValue()));
}
updateGroupState(group, CHANNEL_FORECAST_PRECIPITATION,
new DecimalType(forecast.precipitation.value.floatValue()));
updateGroupState(group, CHANNEL_FORECAST_PROBABILITYOFPRECIPITATION,
new DecimalType(forecast.probabilityOfPrecipitation));
}
}
private void updateTemperature(UnitValue temperature, String group, String channel) {
logger.debug("TEMP {} {} {} {}", group, channel, temperature.unit, temperature.value);
updateGroupState(group, channel, new QuantityType<Temperature>(temperature.value,
"\\u00b0F".equals(temperature.unit) ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
}
private void updateWindspeed(UnitValue wind, String group, String channel) {
updateGroupState(group, channel, new QuantityType<Speed>(wind.value,
"mph".equals(wind.unit) ? ImperialUnits.MILES_PER_HOUR : SIUnits.KILOMETRE_PER_HOUR));
}
private void updateGroupState(String group, String channelID, State state) {
String channelName = group + "#" + channelID;
State oldState = stateMap.put(channelName, state);
if (!state.equals(oldState)) {
ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
logger.debug("updateState updating {} {}", channelUID, state);
updateState(channelUID, state);
}
}
@Nullable
private HydrawiseAccountHandler getAccountHandler() {
Bridge bridge = getBridge();
if (bridge == null) {
logger.warn("No bridge found for thing");
return null;
}
BridgeHandler handler = bridge.getHandler();
if (handler == null) {
logger.warn("No handler found for bridge");
return null;
}
return ((HydrawiseAccountHandler) handler);
}
@Nullable
private HydrawiseGraphQLClient apiClient() {
HydrawiseAccountHandler handler = getAccountHandler();
if (handler == null) {
return null;
} else {
return handler.graphQLClient();
}
}
private DateTimeType secondsToDateTime(Integer seconds) {
Instant instant = Instant.ofEpochSecond(seconds);
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneOffset.UTC);
return new DateTimeType(zdt);
}
private DateTimeType stringToDateTime(String date) {
ZonedDateTime zdt = ZonedDateTime.parse(date, DATE_FORMATTER);
return new DateTimeType(zdt);
}
private QuantityType<Volume> waterFlowToQuantityType(Number flow, String units) {
double waterFlow = flow.doubleValue();
if ("gals".equals(units)) {
waterFlow = waterFlow * 3.785;
}
return new QuantityType<>(waterFlow, Units.LITRE);
}
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hydrawise.internal;
package org.openhab.binding.hydrawise.internal.handler;
import static org.openhab.binding.hydrawise.internal.HydrawiseBindingConstants.*;
@@ -19,23 +19,27 @@ import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
import org.openhab.binding.hydrawise.internal.api.model.LocalScheduleResponse;
import org.openhab.binding.hydrawise.internal.api.model.Relay;
import org.openhab.binding.hydrawise.internal.api.model.Running;
import org.openhab.binding.hydrawise.internal.api.local.HydrawiseLocalApiClient;
import org.openhab.binding.hydrawise.internal.api.local.dto.LocalScheduleResponse;
import org.openhab.binding.hydrawise.internal.api.local.dto.Relay;
import org.openhab.binding.hydrawise.internal.api.local.dto.Running;
import org.openhab.binding.hydrawise.internal.config.HydrawiseLocalConfiguration;
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.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@@ -49,23 +53,22 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HydrawiseHandler} is responsible for handling commands, which are
* The {@link HydrawiseLocalHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public abstract class HydrawiseHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(HydrawiseHandler.class);
public class HydrawiseLocalHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(HydrawiseLocalHandler.class);
protected final Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
protected final Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
private @Nullable ScheduledFuture<?> pollFuture;
private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
private Map<String, Relay> relayMap = Collections.synchronizedMap(new HashMap<>());
/**
* value observed being used by the Hydrawise clients as a max time value,
*/
private static long MAX_RUN_TIME = 157680000;
private static final long MAX_RUN_TIME = 157680000;
/**
* Minimum amount of time we can poll for updates
@@ -86,8 +89,28 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
* Future to poll for updated
*/
public HydrawiseHandler(Thing thing) {
HydrawiseLocalApiClient client;
public HydrawiseLocalHandler(Thing thing, HttpClient httpClient) {
super(thing);
client = new HydrawiseLocalApiClient(httpClient);
}
@Override
public void initialize() {
scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
}
@Override
public void dispose() {
logger.debug("Handler disposed.");
clearPolling();
}
@Override
public void channelLinked(ChannelUID channelUID) {
// clear our cached value so the new channel gets updated on the next poll
stateMap.remove(channelUID.getId());
}
@SuppressWarnings({ "null", "unused" }) // compiler does not like relayMap.get can return null
@@ -120,15 +143,14 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
clearPolling();
switch (channelId) {
case CHANNEL_ZONE_RUN_CUSTOM:
if (!(command instanceof DecimalType)) {
if (!(command instanceof QuantityType<?>)) {
logger.warn("Invalid command type for run custom {}", command.getClass().getName());
return;
}
if (allCommand) {
sendRunAllCommand(((DecimalType) command).intValue());
client.runAllRelays(((QuantityType<?>) command).intValue());
} else {
Objects.requireNonNull(relay);
sendRunCommand(((DecimalType) command).intValue(), relay);
client.runRelay(((QuantityType<?>) command).intValue(), relay.relay);
}
break;
case CHANNEL_ZONE_RUN:
@@ -138,16 +160,15 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
}
if (allCommand) {
if (command == OnOffType.ON) {
sendRunAllCommand();
client.runAllRelays();
} else {
sendStopAllCommand();
client.stopAllRelays();
}
} else {
Objects.requireNonNull(relay);
if (command == OnOffType.ON) {
sendRunCommand(relay);
client.runRelay(relay.relay);
} else {
sendStopCommand(relay);
client.stopRelay(relay.relay);
}
}
break;
@@ -163,46 +184,6 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
}
}
@Override
public void initialize() {
scheduler.schedule(this::configureInternal, 0, TimeUnit.SECONDS);
}
@Override
public void dispose() {
logger.debug("Handler disposed.");
clearPolling();
}
@Override
public void channelLinked(ChannelUID channelUID) {
// clear our cached value so the new channel gets updated on the next poll
stateMap.remove(channelUID.getId());
}
protected abstract void configure()
throws NotConfiguredException, HydrawiseConnectionException, HydrawiseAuthenticationException;
protected abstract void pollController() throws HydrawiseConnectionException, HydrawiseAuthenticationException;
protected abstract void sendRunCommand(int seconds, Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
protected abstract void sendRunCommand(Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
protected abstract void sendStopCommand(Relay relay)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
protected abstract void sendRunAllCommand()
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
protected abstract void sendRunAllCommand(int seconds)
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
protected abstract void sendStopAllCommand()
throws HydrawiseCommandException, HydrawiseConnectionException, HydrawiseAuthenticationException;
protected void updateZones(LocalScheduleResponse status) {
ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
status.relays.forEach(r -> {
@@ -211,12 +192,8 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
logger.trace("Updateing Zone {} {} ", group, r.name);
updateGroupState(group, CHANNEL_ZONE_NAME, new StringType(r.name));
updateGroupState(group, CHANNEL_ZONE_TYPE, new DecimalType(r.type));
updateGroupState(group, CHANNEL_ZONE_TIME,
r.runTimeSeconds != null ? new DecimalType(r.runTimeSeconds) : UnDefType.UNDEF);
String icon = r.icon;
if (icon != null && !icon.isBlank()) {
updateGroupState(group, CHANNEL_ZONE_ICON, new StringType(BASE_IMAGE_URL + icon));
}
updateGroupState(group, CHANNEL_ZONE_STARTTIME,
r.runSeconds != null ? new QuantityType<>(r.runSeconds, Units.SECOND) : UnDefType.UNDEF);
if (r.time >= MAX_RUN_TIME) {
updateGroupState(group, CHANNEL_ZONE_NEXT_RUN_TIME_TIME, UnDefType.UNDEF);
} else {
@@ -228,32 +205,21 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
.filter(z -> Integer.parseInt(z.relayId) == r.relayId.intValue()).findAny();
if (running.isPresent()) {
updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.ON);
updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(running.get().timeLeft));
updateGroupState(group, CHANNEL_ZONE_TIME_LEFT,
new QuantityType<>(running.get().timeLeft, Units.SECOND));
logger.debug("{} Time Left {}", r.name, running.get().timeLeft);
} else {
updateGroupState(group, CHANNEL_ZONE_RUN, OnOffType.OFF);
updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new DecimalType(0));
updateGroupState(group, CHANNEL_ZONE_TIME_LEFT, new QuantityType<>(0, Units.SECOND));
}
updateGroupState(CHANNEL_GROUP_ALLZONES, CHANNEL_ZONE_RUN,
!status.running.isEmpty() ? OnOffType.ON : OnOffType.OFF);
status.running.size() > 0 ? OnOffType.ON : OnOffType.OFF);
});
}
protected void updateGroupState(String group, String channelID, State state) {
String channelName = group + "#" + channelID;
State oldState = stateMap.put(channelName, state);
if (!state.equals(oldState)) {
ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
logger.debug("updateState updating {} {}", channelUID, state);
updateState(channelUID, state);
}
}
@SuppressWarnings("serial")
@NonNullByDefault
protected class NotConfiguredException extends Exception {
NotConfiguredException(String message) {
super(message);
@@ -269,11 +235,19 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
stateMap.clear();
relayMap.clear();
try {
configure();
initPolling(0);
} catch (NotConfiguredException e) {
logger.debug("Configuration error {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
HydrawiseLocalConfiguration configuration = getConfig().as(HydrawiseLocalConfiguration.class);
this.refresh = Math.max(configuration.refresh, MIN_REFRESH_SECONDS);
logger.trace("Connecting to host {}", configuration.host);
client.setCredentials(configuration.host, configuration.username, configuration.password);
LocalScheduleResponse response = client.getLocalSchedule();
if (response != null) {
updateZones(response);
initPolling(refresh);
} else {
logger.debug("Could not connect to service");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Invalid response from service");
}
} catch (HydrawiseConnectionException e) {
logger.debug("Could not connect to service");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
@@ -310,7 +284,10 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
*/
private void pollControllerInternal() {
try {
pollController();
LocalScheduleResponse response = client.getLocalSchedule();
if (response != null) {
updateZones(response);
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
@@ -324,4 +301,14 @@ public abstract class HydrawiseHandler extends BaseThingHandler {
configureInternal();
}
}
private void updateGroupState(String group, String channelID, State state) {
String channelName = group + "#" + channelID;
State oldState = stateMap.put(channelName, state);
if (!state.equals(oldState)) {
ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelName);
logger.debug("updateState updating {} {}", channelUID, state);
updateState(channelUID, state);
}
}
}

View File

@@ -10,12 +10,14 @@
<channels>
<channel id="name" typeId="name"/>
<channel id="icon" typeId="icon"/>
<channel id="time" typeId="time"/>
<channel id="type" typeId="type"/>
<channel id="runcustom" typeId="runcustom"/>
<channel id="run" typeId="run"/>
<channel id="runcustom" typeId="runcustom"/>
<channel id="nextruntime" typeId="nextruntime"/>
<channel id="suspend" typeId="suspend"/>
<channel id="suspenduntil" typeId="suspenduntil"/>
<channel id="timeleft" typeId="timeleft"/>
<channel id="summary" typeId="summary"/>
</channels>
</channel-group-type>
@@ -25,6 +27,8 @@
<channels>
<channel id="runcustom" typeId="runcustom"/>
<channel id="run" typeId="run"/>
<channel id="suspend" typeId="suspend"/>
<channel id="suspenduntil" typeId="suspenduntil"/>
</channels>
</channel-group-type>
@@ -34,11 +38,11 @@
<channels>
<channel id="name" typeId="name"/>
<channel id="input" typeId="input"/>
<channel id="mode" typeId="mode"/>
<channel id="timer" typeId="timer"/>
<channel id="delay" typeId="delay"/>
<channel id="offtimer" typeId="offtimer"/>
<channel id="offlevel" typeId="offlevel"/>
<channel id="active" typeId="active"/>
<channel id="waterflow" typeId="waterflow"/>
</channels>
</channel-group-type>
@@ -49,13 +53,43 @@
<channel id="temperaturehigh" typeId="temperaturehigh"/>
<channel id="temperaturelow" typeId="temperaturelow"/>
<channel id="conditions" typeId="conditions"/>
<channel id="day" typeId="day"/>
<channel id="time" typeId="time"/>
<channel id="humidity" typeId="humidity"/>
<channel id="wind" typeId="wind"/>
<channel id="evapotranspiration" typeId="evapotranspiration"/>
<channel id="precipitation" typeId="precipitation"/>
<channel id="probabilityofprecipitation" typeId="probabilityofprecipitation"/>
</channels>
</channel-group-type>
<channel-group-type id="system">
<label>System</label>
<description>Controller system data</description>
<channels>
<channel id="name" typeId="name"/>
<channel id="summary" typeId="summary"/>
<channel id="lastcontacttime" typeId="lastcontacttime"/>
</channels>
</channel-group-type>
<!-- Controller -->
<channel-type id="lastcontacttime" advanced="true">
<item-type>DateTime</item-type>
<label>Last Contact Time</label>
<description>Last contact time of a controller</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="summary">
<item-type>String</item-type>
<label>Status Summary</label>
<description>Status summary</description>
<state readOnly="true"></state>
</channel-type>
<!-- Zones -->
<channel-type id="name">
<item-type>String</item-type>
<label>Name</label>
@@ -70,10 +104,17 @@
<state readOnly="true"></state>
</channel-type>
<channel-type id="time" advanced="true">
<item-type>Number</item-type>
<channel-type id="starttime" advanced="true">
<item-type>DateTime</item-type>
<label>Start Time</label>
<description>Zone start time in seconds</description>
<description>Next zone start time</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="duration" advanced="true">
<item-type>Number:Time</item-type>
<label>Duration</label>
<description>Next start duration</description>
<state readOnly="true"></state>
</channel-type>
@@ -87,7 +128,7 @@
<channel-type id="nextruntime">
<item-type>DateTime</item-type>
<label>Next Run Time</label>
<description>Next time this zone is scheduled to run</description>
<description>The next time this zone is scheduled to run</description>
<state readOnly="true"></state>
</channel-type>
@@ -98,13 +139,25 @@
</channel-type>
<channel-type id="runcustom">
<item-type>Number</item-type>
<label>Run Zones With Custom Duration </label>
<description>Run zones now for a custom duration of time in seconds</description>
<item-type>Number:Time</item-type>
<label>Run Zones With Custom Duration</label>
<description>Run zones now for a custom duration</description>
</channel-type>
<channel-type id="suspend">
<item-type>Switch</item-type>
<label>Suspend Zones</label>
<description>Suspends or resumes zones</description>
</channel-type>
<channel-type id="suspenduntil">
<item-type>DateTime</item-type>
<label>Suspend Zones</label>
<description>Suspends zones until this date</description>
</channel-type>
<channel-type id="timeleft">
<item-type>Number</item-type>
<item-type>Number:Time</item-type>
<label>Time Left Seconds</label>
<description>Time left that zone will run for</description>
<state readOnly="true"></state>
@@ -126,22 +179,15 @@
<state readOnly="true"></state>
</channel-type>
<channel-type id="mode" advanced="true">
<item-type>Number</item-type>
<label>Mode</label>
<description>Sensor mode</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="timer" advanced="true">
<item-type>Number</item-type>
<label>Timer</label>
<description>Sensor timer</description>
<channel-type id="delay" advanced="true">
<item-type>Number:Time</item-type>
<label>Delay</label>
<description>Sensor delay</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="offtimer" advanced="true">
<item-type>Number</item-type>
<item-type>Number:Time</item-type>
<label>Off Timer</label>
<description>Sensor off timer</description>
<state readOnly="true"></state>
@@ -160,6 +206,7 @@
<description>Sensor off level</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="active">
<item-type>Switch</item-type>
<label>Active</label>
@@ -167,6 +214,13 @@
<state readOnly="true"></state>
</channel-type>
<channel-type id="waterflow" advanced="true">
<item-type>Number:Volume</item-type>
<label>Water Flow</label>
<description>Sensor water flow</description>
<state readOnly="true"></state>
</channel-type>
<!-- Weather Forecast -->
<channel-type id="temperaturehigh">
<item-type>Number:Temperature</item-type>
@@ -198,10 +252,10 @@
<state readOnly="true"></state>
</channel-type>
<channel-type id="day">
<item-type>String</item-type>
<label>Day of Week</label>
<description>Day of week for the weather forecast</description>
<channel-type id="time">
<item-type>DateTime</item-type>
<label>Forecast Time</label>
<description>Forecast date and time</description>
<state readOnly="true"></state>
</channel-type>
@@ -220,4 +274,25 @@
<category>Wind</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="evapotranspiration">
<item-type>Number</item-type>
<label>Evapotranspiration</label>
<description>Evapotranspiration amount</description>
<state readOnly="true" pattern="%.1f"/>
</channel-type>
<channel-type id="precipitation">
<item-type>Number</item-type>
<label>Precipitation</label>
<description>Precipitation amount</description>
<state readOnly="true" pattern="%.1f"/>
</channel-type>
<channel-type id="probabilityofprecipitation">
<item-type>Number</item-type>
<label>Probability Of Precipitation</label>
<description>Probability of precipitation percentage</description>
<state readOnly="true" pattern="%d%%"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -4,14 +4,50 @@
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">
<!-- Sample Thing Type -->
<thing-type id="cloud">
<label>Hydrawise Cloud Thing</label>
<description>Hydrawise cloud connected irrigation system</description>
<bridge-type id="account">
<label>Hydrawise Account Thing</label>
<description>Hydrawise account</description>
<config-description>
<parameter name="userName" type="text" required="true">
<label>User Name</label>
<description>Your Hydrawise account user name</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<context>password</context>
<description>Your Hydrawise account password, for security this will not be saved after the first login attempt
unless the "Save Password" option is enabled</description>
</parameter>
<parameter name="savePassword" type="boolean" required="false">
<label>Save Password</label>
<description>By default, the password will not be persisted after the first login attempt unless this is enabled</description>
<default>false</default>
</parameter>
<parameter name="refresh" type="integer" required="false" min="30" unit="s">
<label>Refresh interval</label>
<description>Specifies the refresh interval in seconds</description>
<default>60</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="controller">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Hydrawise Controller Thing</label>
<description>Hydrawise connected irrigation controller</description>
<!-- Until we have https://github.com/eclipse/smarthome/issues/1118 fixed, we need to list all possible channel groups.
Once this is fixed we can dynamically add them to the thing and not list them here. -->
<channel-groups>
<!-- System -->
<channel-group id="system" typeId="system"/>
<!-- Sensors -->
<channel-group id="sensor1" typeId="sensor">
<label>Sensor 1</label>
<description>Sensor 1</description>
@@ -29,6 +65,8 @@
<description>Sensor 4</description>
</channel-group>
<!-- Forecasts -->
<channel-group id="forecast1" typeId="forecast">
<label>Today's Weather</label>
<description>Today's weather forecast</description>
@@ -41,13 +79,13 @@
<label>Day 3 Weather</label>
<description>Day 3 weather forecast</description>
</channel-group>
<channel-group id="forecast4" typeId="forecast">
<label>Day 4 Weather</label>
<description>Day 4 weather forecast</description>
</channel-group>
<!-- All Zones -->
<channel-group id="allzones" typeId="allzones"/>
<!-- Zones -->
<channel-group id="zone1" typeId="zone">
<label>Zone 1</label>
<description>Sprinkler Zone 1</description>
@@ -194,19 +232,9 @@
</channel-group>
</channel-groups>
<config-description>
<parameter name="apiKey" type="text" required="true">
<label>API Key</label>
<description>API Key from https://app.hydrawise.com/config/account</description>
</parameter>
<parameter name="refresh" type="integer" required="true">
<label>Refresh interval</label>
<description>Specifies the refresh interval in seconds</description>
<default>30</default>
</parameter>
<parameter name="controllerId" type="integer" required="false">
<label>Optional Controller ID interval</label>
<description>Optional parameter to specify the Hydrawise controller ID if you have more then one associated with
your account.
<parameter name="controllerId" type="integer" required="true">
<label>Controller ID</label>
<description>The ID of a cloud connected irrigation controller
</description>
</parameter>
</config-description>
@@ -214,7 +242,7 @@
<thing-type id="local">
<label>Hydrawise Local Thing</label>
<description>Hydrawise local connected irrigation system</description>
<description>Hydrawise local connected irrigation controller</description>
<channel-groups>
<channel-group id="zone1" typeId="zone">
<label>Zone 1</label>
@@ -383,6 +411,4 @@
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,116 @@
{
me {
email
lastContact
controllers {
id
name
status {
summary
online
lastContact {
timestamp
}
}
location {
coordinates {
latitude
longitude
}
forecast(days: 3) {
time
updateTime
conditions
averageWindSpeed {
value
unit
}
highTemperature {
value
unit
}
lowTemperature {
value
unit
}
probabilityOfPrecipitation
precipitation {
value
unit
}
averageHumidity
}
}
zones {
id
name
status {
suspendedUntil {
timestamp
}
}
icon {
id
fileName
customImage {
id
url
}
}
number {
value
label
}
scheduledRuns {
summary
currentRun{
id
startTime {
timestamp
}
endTime {
timestamp
}
duration
status {
value
label
}
}
nextRun {
id
startTime {
timestamp
}
endTime {
timestamp
}
duration
}
}
}
sensors {
id
name
input {
number
label
}
status {
active
waterFlow {
value
unit
}
}
model {
modeType
active
offLevel
offTimer
delay
}
}
}
}
}