[AirQuality] Enhance API error handling (#14602)

* Enhancing API error handling

---------

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2023-04-23 11:14:10 +02:00 committed by GitHub
parent 275329d485
commit edaf9581c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 72 deletions

View File

@ -23,24 +23,18 @@ import org.eclipse.jdt.annotation.Nullable;
@NonNullByDefault @NonNullByDefault
public class AirQualityException extends Exception { public class AirQualityException extends Exception {
private static final long serialVersionUID = -3398100220952729815L; private static final long serialVersionUID = -3398100220952729815L;
private int statusCode = -1;
public AirQualityException(String message, Exception e) { public AirQualityException(String message, Exception e) {
super(message, e); super(message, e);
} }
public AirQualityException(String message) { public AirQualityException(String message, Object... params) {
super(message); super(String.format(message, params));
}
public int getStatusCode() {
return statusCode;
} }
@Override @Override
public @Nullable String getMessage() { public @Nullable String getMessage() {
String message = super.getMessage(); String message = super.getMessage();
return message == null ? null return message == null ? null : String.format("Rest call failed: message=%s", message);
: String.format("Rest call failed: statusCode=%d, message=%s", statusCode, message);
} }
} }

View File

@ -13,13 +13,13 @@
package org.openhab.binding.airquality.internal.api; package org.openhab.binding.airquality.internal.api;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.airquality.internal.AirQualityException; import org.openhab.binding.airquality.internal.AirQualityException;
import org.openhab.binding.airquality.internal.api.dto.AirQualityData; import org.openhab.binding.airquality.internal.api.dto.AirQualityData;
import org.openhab.binding.airquality.internal.api.dto.AirQualityResponse; import org.openhab.binding.airquality.internal.api.dto.AirQualityResponse;
import org.openhab.binding.airquality.internal.api.dto.AirQualityResponse.ResponseStatus;
import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -66,21 +66,24 @@ public class ApiBridge {
* @return an air quality data object mapping the JSON response * @return an air quality data object mapping the JSON response
* @throws AirQualityException * @throws AirQualityException
*/ */
public AirQualityData getData(int stationId, String location, int retryCounter) throws AirQualityException { public AirQualityData getData(int stationId, String location) throws AirQualityException {
String urlStr = buildRequestURL(apiKey, stationId, location); String urlStr = buildRequestURL(apiKey, stationId, location);
logger.debug("URL = {}", urlStr); logger.debug("URL = {}", urlStr);
try { try {
String response = HttpUtil.executeUrl("GET", urlStr, null, null, null, REQUEST_TIMEOUT_MS); String response = HttpUtil.executeUrl("GET", urlStr, null, null, null, REQUEST_TIMEOUT_MS);
logger.debug("aqiResponse = {}", response); if (response != null) {
AirQualityResponse result = GSON.fromJson(response, AirQualityResponse.class); logger.debug("aqiResponse = {}", response);
if (result != null && result.getStatus() == ResponseStatus.OK) { AirQualityResponse result = GSON.fromJson(response, AirQualityResponse.class);
return result.getData(); if (result != null) {
} else if (retryCounter == 0) { String error = result.getErrorMessage();
logger.debug("Error in aqicn.org, retrying once"); if (error.isEmpty()) {
return getData(stationId, location, retryCounter + 1); return Objects.requireNonNull(result.getData());
}
throw new AirQualityException("Error raised : %s", error);
}
} }
throw new AirQualityException("Error in aqicn.org response: Missing data sub-object"); throw new JsonSyntaxException("API response is null");
} catch (IOException | JsonSyntaxException e) { } catch (IOException | JsonSyntaxException e) {
throw new AirQualityException("Communication error", e); throw new AirQualityException("Communication error", e);
} }

View File

@ -24,17 +24,17 @@ import org.openhab.core.types.State;
*/ */
@NonNullByDefault @NonNullByDefault
public enum Appreciation { public enum Appreciation {
GOOD(HSBType.fromRGB(0, 228, 0)), GOOD(0, 228, 0),
MODERATE(HSBType.fromRGB(255, 255, 0)), MODERATE(255, 255, 0),
UNHEALTHY_FSG(HSBType.fromRGB(255, 126, 0)), UNHEALTHY_FSG(255, 126, 0),
UNHEALTHY(HSBType.fromRGB(255, 0, 0)), UNHEALTHY(255, 0, 0),
VERY_UNHEALTHY(HSBType.fromRGB(143, 63, 151)), VERY_UNHEALTHY(143, 63, 151),
HAZARDOUS(HSBType.fromRGB(126, 0, 35)); HAZARDOUS(126, 0, 35);
private HSBType color; private HSBType color;
Appreciation(HSBType color) { Appreciation(int r, int g, int b) {
this.color = color; this.color = HSBType.fromRGB(r, g, b);
} }
public State getColor() { public State getColor() {

View File

@ -14,9 +14,11 @@ package org.openhab.binding.airquality.internal.api.dto;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.airquality.internal.api.Pollutant; import org.openhab.binding.airquality.internal.api.Pollutant;
/** /**
@ -26,12 +28,13 @@ import org.openhab.binding.airquality.internal.api.Pollutant;
* @author Kuba Wolanin - Initial contribution * @author Kuba Wolanin - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class AirQualityData { public class AirQualityData extends ResponseRoot {
private int aqi; private int aqi;
private int idx; private int idx;
private @NonNullByDefault({}) AirQualityTime time; private @Nullable AirQualityTime time;
private @NonNullByDefault({}) AirQualityCity city; private @Nullable AirQualityCity city;
private List<Attribution> attributions = List.of(); private List<Attribution> attributions = List.of();
private Map<String, AirQualityValue> iaqi = Map.of(); private Map<String, AirQualityValue> iaqi = Map.of();
private String dominentpol = ""; private String dominentpol = "";
@ -59,8 +62,8 @@ public class AirQualityData {
* *
* @return {AirQualityJsonTime} * @return {AirQualityJsonTime}
*/ */
public AirQualityTime getTime() { public Optional<AirQualityTime> getTime() {
return time; return Optional.ofNullable(time);
} }
/** /**
@ -68,8 +71,8 @@ public class AirQualityData {
* *
* @return {AirQualityJsonCity} * @return {AirQualityJsonCity}
*/ */
public AirQualityCity getCity() { public Optional<AirQualityCity> getCity() {
return city; return Optional.ofNullable(city);
} }
/** /**

View File

@ -13,8 +13,7 @@
package org.openhab.binding.airquality.internal.api.dto; package org.openhab.binding.airquality.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/** /**
* The {@link AirQualityResponse} is the Java class used to map the JSON * The {@link AirQualityResponse} is the Java class used to map the JSON
@ -23,24 +22,40 @@ import com.google.gson.annotations.SerializedName;
* @author Kuba Wolanin - Initial contribution * @author Kuba Wolanin - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class AirQualityResponse { public class AirQualityResponse extends ResponseRoot {
public static enum ResponseStatus { private @Nullable AirQualityData data;
NONE,
@SerializedName("error")
ERROR,
@SerializedName("ok")
OK;
}
private ResponseStatus status = ResponseStatus.NONE; public @Nullable AirQualityData getData() {
private @NonNullByDefault({}) AirQualityData data;
public ResponseStatus getStatus() {
return status;
}
public AirQualityData getData() {
return data; return data;
} }
private ResponseStatus getStatus() {
AirQualityData localData = data;
return status == ResponseStatus.OK && localData != null && localData.status == ResponseStatus.OK
? ResponseStatus.OK
: ResponseStatus.ERROR;
}
public String getErrorMessage() {
if (getStatus() != ResponseStatus.OK) {
String localMsg = msg;
if (localMsg != null) {
return localMsg;
} else {
AirQualityData localData = data;
if (localData != null) {
localMsg = localData.msg;
if (localMsg != null) {
return localMsg;
} else {
return "Unknown error";
}
} else {
return "No data provided";
}
}
}
return "";
}
} }

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2023 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.airquality.internal.api.dto;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ResponseRoot} is the common part of Air Quality API response objectss
*
* @author Gaël L'hopital - Initial contribution
*/
public class ResponseRoot {
public static enum ResponseStatus {
@SerializedName("error")
ERROR,
@SerializedName("ok")
OK;
}
protected ResponseStatus status = ResponseStatus.OK;
protected @Nullable String msg;
}

View File

@ -84,14 +84,14 @@ public class AirQualityDiscoveryService extends AbstractDiscoveryService impleme
PointType location = provider.getLocation(); PointType location = provider.getLocation();
AirQualityBridgeHandler bridge = this.bridgeHandler; AirQualityBridgeHandler bridge = this.bridgeHandler;
if (location == null || bridge == null) { if (location == null || bridge == null) {
logger.debug("LocationProvider.getLocation() is not set -> Will not provide any discovery results"); logger.info("openHAB server location is not defined, will not provide any discovery results");
return; return;
} }
createResults(location, bridge.getThing().getUID()); createResults(location, bridge.getThing().getUID());
} }
} }
public void createResults(PointType location, ThingUID bridgeUID) { private void createResults(PointType location, ThingUID bridgeUID) {
ThingUID localAirQualityThing = new ThingUID(THING_TYPE_STATION, bridgeUID, LOCAL); ThingUID localAirQualityThing = new ThingUID(THING_TYPE_STATION, bridgeUID, LOCAL);
thingDiscovered(DiscoveryResultBuilder.create(localAirQualityThing).withLabel("Local Air Quality") thingDiscovered(DiscoveryResultBuilder.create(localAirQualityThing).withLabel("Local Air Quality")
.withProperty(LOCATION, String.format("%s,%s", location.getLatitude(), location.getLongitude())) .withProperty(LOCATION, String.format("%s,%s", location.getLatitude(), location.getLongitude()))

View File

@ -13,7 +13,7 @@
package org.openhab.binding.airquality.internal.handler; package org.openhab.binding.airquality.internal.handler;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -67,7 +67,7 @@ public class AirQualityBridgeHandler extends BaseBridgeHandler {
@Override @Override
public Collection<Class<? extends ThingHandlerService>> getServices() { public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(AirQualityDiscoveryService.class); return Set.of(AirQualityDiscoveryService.class);
} }
public LocationProvider getLocationProvider() { public LocationProvider getLocationProvider() {

View File

@ -110,13 +110,13 @@ public class AirQualityStationHandler extends BaseThingHandler {
private void discoverAttributes() { private void discoverAttributes() {
getAirQualityData().ifPresent(data -> { getAirQualityData().ifPresent(data -> {
// Update thing properties // Update thing properties
Map<String, String> properties = new HashMap<>(); Map<String, String> properties = new HashMap<>(Map.of(ATTRIBUTIONS, data.getAttributions()));
properties.put(ATTRIBUTIONS, data.getAttributions());
PointType serverLocation = locationProvider.getLocation(); PointType serverLocation = locationProvider.getLocation();
if (serverLocation != null) { if (serverLocation != null) {
PointType stationLocation = new PointType(data.getCity().getGeo()); data.getCity().ifPresent(city -> {
double distance = serverLocation.distanceFrom(stationLocation).doubleValue(); double distance = serverLocation.distanceFrom(new PointType(city.getGeo())).doubleValue();
properties.put(DISTANCE, new QuantityType<>(distance / 1000, KILO(SIUnits.METRE)).toString()); properties.put(DISTANCE, new QuantityType<>(distance / 1000, KILO(SIUnits.METRE)).toString());
});
} }
// Search and remove missing pollutant channels // Search and remove missing pollutant channels
@ -132,8 +132,8 @@ public class AirQualityStationHandler extends BaseThingHandler {
config.put(AirQualityConfiguration.STATION_ID, data.getStationId()); config.put(AirQualityConfiguration.STATION_ID, data.getStationId());
ThingBuilder thingBuilder = editThing(); ThingBuilder thingBuilder = editThing();
thingBuilder.withChannels(channels).withConfiguration(config).withProperties(properties) thingBuilder.withChannels(channels).withConfiguration(config).withProperties(properties);
.withLocation(data.getCity().getName()); data.getCity().map(city -> thingBuilder.withLocation(city.getName()));
updateThing(thingBuilder.build()); updateThing(thingBuilder.build());
}); });
} }
@ -190,7 +190,7 @@ public class AirQualityStationHandler extends BaseThingHandler {
if (apiBridge != null) { if (apiBridge != null) {
AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class); AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
try { try {
result = apiBridge.getData(config.stationId, config.location, 0); result = apiBridge.getData(config.stationId, config.location);
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
} catch (AirQualityException e) { } catch (AirQualityException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
@ -253,24 +253,26 @@ public class AirQualityStationHandler extends BaseThingHandler {
switch (channelId) { switch (channelId) {
case TEMPERATURE: case TEMPERATURE:
double temp = data.getIaqiValue("t"); double temp = data.getIaqiValue("t");
return temp != -1 ? new QuantityType<>(temp, SIUnits.CELSIUS) : UnDefType.UNDEF; return temp != -1 ? new QuantityType<>(temp, SIUnits.CELSIUS) : UnDefType.NULL;
case PRESSURE: case PRESSURE:
double press = data.getIaqiValue("p"); double press = data.getIaqiValue("p");
return press != -1 ? new QuantityType<>(press, HECTO(SIUnits.PASCAL)) : UnDefType.UNDEF; return press != -1 ? new QuantityType<>(press, HECTO(SIUnits.PASCAL)) : UnDefType.NULL;
case HUMIDITY: case HUMIDITY:
double hum = data.getIaqiValue("h"); double hum = data.getIaqiValue("h");
return hum != -1 ? new QuantityType<>(hum, Units.PERCENT) : UnDefType.UNDEF; return hum != -1 ? new QuantityType<>(hum, Units.PERCENT) : UnDefType.NULL;
case TIMESTAMP: case TIMESTAMP:
return new DateTimeType( return data.getTime()
data.getTime().getObservationTime().withZoneSameLocal(timeZoneProvider.getTimeZone())); .map(time -> (State) new DateTimeType(
time.getObservationTime().withZoneSameLocal(timeZoneProvider.getTimeZone())))
.orElse(UnDefType.NULL);
case DOMINENT: case DOMINENT:
return new StringType(data.getDominentPol()); return new StringType(data.getDominentPol());
case DEW_POINT: case DEW_POINT:
double dp = data.getIaqiValue("dew"); double dp = data.getIaqiValue("dew");
return dp != -1 ? new QuantityType<>(dp, SIUnits.CELSIUS) : UnDefType.UNDEF; return dp != -1 ? new QuantityType<>(dp, SIUnits.CELSIUS) : UnDefType.NULL;
case WIND_SPEED: case WIND_SPEED:
double w = data.getIaqiValue("w"); double w = data.getIaqiValue("w");
return w != -1 ? new QuantityType<>(w, Units.METRE_PER_SECOND) : UnDefType.UNDEF; return w != -1 ? new QuantityType<>(w, Units.METRE_PER_SECOND) : UnDefType.NULL;
default: default:
if (groupId != null) { if (groupId != null) {
double idx = -1; double idx = -1;
@ -283,7 +285,7 @@ public class AirQualityStationHandler extends BaseThingHandler {
} }
return indexedValue(channelId, idx, pollutant); return indexedValue(channelId, idx, pollutant);
} }
return UnDefType.UNDEF; return UnDefType.NULL;
} }
} }