Skip to content

Commit

Permalink
[hydrawise] Various Fixes (openhab#17345)
Browse files Browse the repository at this point in the history
* Workaround for a bad response from the Hydrawise API

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
  • Loading branch information
digitaldan authored and matchews committed Oct 18, 2024
1 parent 99581e7 commit 2383d43
Show file tree
Hide file tree
Showing 12 changed files with 333 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,32 @@
public class HydrawiseConnectionException extends Exception {
private static final long serialVersionUID = 1L;

private int code = 0;
private String response = "";

public HydrawiseConnectionException(Exception e) {
super(e);
}

public HydrawiseConnectionException(String message) {
super(message);
}

public HydrawiseConnectionException(String message, int code, String response) {
super(message);
this.code = code;
this.response = response;
}

public static long getSerialversionuid() {
return serialVersionUID;
}

public int getCode() {
return code;
}

public String getResponse() {
return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
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.Hardware;
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;
Expand Down Expand Up @@ -77,7 +78,8 @@ public class HydrawiseGraphQLClient {
.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();
.registerTypeAdapter(ControllerStatus.class, new ResponseDeserializer<ControllerStatus>())
.registerTypeAdapter(Hardware.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 }";
Expand All @@ -94,6 +96,7 @@ public class HydrawiseGraphQLClient {
private final HttpClient httpClient;
private final OAuthClientService oAuthService;
private String queryString = "";
private String weatherString = "";

public HydrawiseGraphQLClient(HttpClient httpClient, OAuthClientService oAuthService) {
this.httpClient = httpClient;
Expand All @@ -110,19 +113,48 @@ public HydrawiseGraphQLClient(HttpClient httpClient, OAuthClientService oAuthSer
public @Nullable QueryResponse queryControllers()
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
try {
QueryRequest query = new QueryRequest(getQueryString());
String queryJson = gson.toJson(query);
String response = sendGraphQLQuery(queryJson);
try {
return gson.fromJson(response, QueryResponse.class);
} catch (JsonSyntaxException e) {
throw new HydrawiseConnectionException("Invalid Response: " + response);
}
return queryRequest(getQueryString());
} catch (IOException e) {
throw new HydrawiseConnectionException(e);
}
}

/**
* Sends a GrapQL query for controller data
*
* @return QueryResponse
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
*/
public @Nullable QueryResponse queryWeather()
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
try {
return queryRequest(getWeatherString());
} catch (IOException e) {
throw new HydrawiseConnectionException(e);
}
}

/**
* Sends a GrapQL query for controller data
*
* @param queryString
* @return QueryResponse
* @throws HydrawiseConnectionException
* @throws HydrawiseAuthenticationException
*/
private @Nullable QueryResponse queryRequest(String queryString)
throws HydrawiseConnectionException, HydrawiseAuthenticationException {
QueryRequest query = new QueryRequest(queryString);
String queryJson = gson.toJson(query);
String response = sendGraphQLQuery(queryJson);
try {
return gson.fromJson(response, QueryResponse.class);
} catch (JsonSyntaxException e) {
throw new HydrawiseConnectionException("Invalid Response: " + response);
}
}

/***
* Stops a given relay
*
Expand Down Expand Up @@ -313,7 +345,8 @@ public void onFailure(@Nullable Response response, @Nullable Throwable failure)
int statusCode = response.getStatus();
if (!HttpStatus.isSuccess(statusCode)) {
throw new HydrawiseConnectionException(
"Request failed with HTTP status code: " + statusCode + " response: " + stringResponse);
"Request failed with HTTP status code: " + statusCode + " response: " + stringResponse,
statusCode, stringResponse);
}
return stringResponse;
} catch (InterruptedException | TimeoutException | OAuthException | IOException e) {
Expand All @@ -338,15 +371,25 @@ public void onFailure(@Nullable Response response, @Nullable Throwable failure)

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"));
}
queryString = getResourceString("query.graphql");
}
return queryString;
}

private String getWeatherString() throws IOException {
if (weatherString.isBlank()) {
weatherString = getResourceString("weather.graphql");
}
return weatherString;
}

private String getResourceString(String name) throws IOException {
try (InputStream inputStream = HydrawiseGraphQLClient.class.getClassLoader().getResourceAsStream(name);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
return bufferedReader.lines().collect(Collectors.joining("\n"));
}
}

class ResponseDeserializer<T> implements JsonDeserializer<T> {
@Override
@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public class Controller {
public Integer id;
public String name;
public ControllerStatus status;
public Hardware hardware;
public Location location;
public List<Zone> zones = null;
public List<Sensor> sensors = null;
public List<Forecast> forecast = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2024 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 Hardware {
public String version;
public Model model;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2024 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 Model {
public Integer maxZones;
public String name;
public String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,23 @@
*/
package org.openhab.binding.hydrawise.internal.discovery;

import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Set;

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.AbstractThingHandlerDiscoveryService;
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;
import org.osgi.service.component.annotations.ServiceScope;

/**
*
Expand All @@ -36,33 +37,44 @@
*/

@NonNullByDefault
@Component(scope = ServiceScope.PROTOTYPE, service = ThingHandlerService.class)
public class HydrawiseCloudControllerDiscoveryService
extends AbstractThingHandlerDiscoveryService<HydrawiseAccountHandler> implements HydrawiseControllerListener {
@Component(service = ThingHandlerService.class)
public class HydrawiseCloudControllerDiscoveryService extends AbstractDiscoveryService
implements HydrawiseControllerListener, ThingHandlerService {

private static final int TIMEOUT = 5;
@Nullable
HydrawiseAccountHandler handler;

public HydrawiseCloudControllerDiscoveryService() {
super(HydrawiseAccountHandler.class, Set.of(HydrawiseBindingConstants.THING_TYPE_CONTROLLER), TIMEOUT, true);
super(Set.of(HydrawiseBindingConstants.THING_TYPE_CONTROLLER), TIMEOUT, true);
}

@Override
protected void startScan() {
Customer data = thingHandler.lastData();
if (data != null) {
data.controllers.forEach(controller -> addDiscoveryResults(controller));
HydrawiseAccountHandler localHandler = this.handler;
if (localHandler != null) {
Customer data = localHandler.lastData();
if (data != null) {
data.controllers.forEach(controller -> addDiscoveryResults(controller));
}
}
}

@Override
public void dispose() {
super.dispose();
removeOlderResults(Instant.now().toEpochMilli(), thingHandler.getThing().getUID());
public void deactivate() {
HydrawiseAccountHandler localHandler = this.handler;
if (localHandler != null) {
removeOlderResults(new Date().getTime(), localHandler.getThing().getUID());
}
}

@Override
protected synchronized void stopScan() {
super.stopScan();
removeOlderResults(getTimestampOfLastScan(), thingHandler.getThing().getUID());
HydrawiseAccountHandler localHandler = this.handler;
if (localHandler != null) {
removeOlderResults(getTimestampOfLastScan(), localHandler.getThing().getUID());
}
}

@Override
Expand All @@ -71,19 +83,27 @@ public void onData(List<Controller> controllers) {
}

@Override
public void initialize() {
thingHandler.addControllerListeners(this);
super.initialize();
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) {
String label = String.format("Hydrawise Controller %s", controller.name);
int id = controller.id;
ThingUID bridgeUID = thingHandler.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(HydrawiseBindingConstants.CONFIG_CONTROLLER_ID).build());
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(HydrawiseBindingConstants.CONFIG_CONTROLLER_ID).build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class HydrawiseAccountHandler extends BaseBridgeHandler implements Access
*/
private static final int MIN_REFRESH_SECONDS = 30;
private static final int TOKEN_REFRESH_SECONDS = 60;
private static final int WEATHER_REFRESH_MILLIS = 60 * 60 * 1000; // 1 hour
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";
Expand All @@ -77,6 +78,7 @@ public class HydrawiseAccountHandler extends BaseBridgeHandler implements Access
private @Nullable ScheduledFuture<?> pollFuture;
private @Nullable ScheduledFuture<?> tokenFuture;
private @Nullable Customer lastData;
private long lastWeatherUpdate;
private int refresh;

public HydrawiseAccountHandler(final Bridge bridge, final HttpClient httpClient, final OAuthFactory oAuthFactory) {
Expand Down Expand Up @@ -228,6 +230,11 @@ private void poll() {
}

private void poll(boolean retry) {
HydrawiseGraphQLClient apiClient = this.apiClient;
if (apiClient == null) {
logger.debug("apiclient not initalized");
return;
}
try {
QueryResponse response = apiClient.queryControllers();
if (response == null) {
Expand All @@ -240,6 +247,21 @@ private void poll(boolean retry) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
long currentTime = System.currentTimeMillis();
if (currentTime > lastWeatherUpdate + WEATHER_REFRESH_MILLIS) {
lastWeatherUpdate = currentTime;
try {
QueryResponse weatherResponse = apiClient.queryWeather();
if (weatherResponse != null) {
response.data.me.controllers.forEach(controller -> {
weatherResponse.data.me.controllers.stream().filter(c -> c.id.equals(controller.id))
.findFirst().ifPresent(c -> controller.location.forecast = c.location.forecast);
});
}
} catch (HydrawiseConnectionException e) {
logger.debug("Weather data is not supported", e);
}
}
lastData = response.data.me;
synchronized (controllerListeners) {
controllerListeners.forEach(listener -> {
Expand Down
Loading

0 comments on commit 2383d43

Please sign in to comment.