@NonNullByDefault
public class AirQualityException extends Exception {
private static final long serialVersionUID = -3398100220952729815L;
- private int statusCode = -1;
public AirQualityException(String message, Exception e) {
super(message, e);
}
- public AirQualityException(String message) {
- super(message);
- }
-
- public int getStatusCode() {
- return statusCode;
+ public AirQualityException(String message, Object... params) {
+ super(String.format(message, params));
}
@Override
public @Nullable String getMessage() {
String message = super.getMessage();
- return message == null ? null
- : String.format("Rest call failed: statusCode=%d, message=%s", statusCode, message);
+ return message == null ? null : String.format("Rest call failed: message=%s", message);
}
}
package org.openhab.binding.airquality.internal.api;
import java.io.IOException;
+import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.airquality.internal.AirQualityException;
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.ResponseStatus;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* @return an air quality data object mapping the JSON response
* @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);
logger.debug("URL = {}", urlStr);
try {
String response = HttpUtil.executeUrl("GET", urlStr, null, null, null, REQUEST_TIMEOUT_MS);
- logger.debug("aqiResponse = {}", response);
- AirQualityResponse result = GSON.fromJson(response, AirQualityResponse.class);
- if (result != null && result.getStatus() == ResponseStatus.OK) {
- return result.getData();
- } else if (retryCounter == 0) {
- logger.debug("Error in aqicn.org, retrying once");
- return getData(stationId, location, retryCounter + 1);
+ if (response != null) {
+ logger.debug("aqiResponse = {}", response);
+ AirQualityResponse result = GSON.fromJson(response, AirQualityResponse.class);
+ if (result != null) {
+ String error = result.getErrorMessage();
+ if (error.isEmpty()) {
+ 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) {
throw new AirQualityException("Communication error", e);
}
*/
@NonNullByDefault
public enum Appreciation {
- GOOD(HSBType.fromRGB(0, 228, 0)),
- MODERATE(HSBType.fromRGB(255, 255, 0)),
- UNHEALTHY_FSG(HSBType.fromRGB(255, 126, 0)),
- UNHEALTHY(HSBType.fromRGB(255, 0, 0)),
- VERY_UNHEALTHY(HSBType.fromRGB(143, 63, 151)),
- HAZARDOUS(HSBType.fromRGB(126, 0, 35));
+ GOOD(0, 228, 0),
+ MODERATE(255, 255, 0),
+ UNHEALTHY_FSG(255, 126, 0),
+ UNHEALTHY(255, 0, 0),
+ VERY_UNHEALTHY(143, 63, 151),
+ HAZARDOUS(126, 0, 35);
private HSBType color;
- Appreciation(HSBType color) {
- this.color = color;
+ Appreciation(int r, int g, int b) {
+ this.color = HSBType.fromRGB(r, g, b);
}
public State getColor() {
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.airquality.internal.api.Pollutant;
/**
* @author Kuba Wolanin - Initial contribution
*/
@NonNullByDefault
-public class AirQualityData {
+public class AirQualityData extends ResponseRoot {
+
private int aqi;
private int idx;
- private @NonNullByDefault({}) AirQualityTime time;
- private @NonNullByDefault({}) AirQualityCity city;
+ private @Nullable AirQualityTime time;
+ private @Nullable AirQualityCity city;
private List<Attribution> attributions = List.of();
private Map<String, AirQualityValue> iaqi = Map.of();
private String dominentpol = "";
*
* @return {AirQualityJsonTime}
*/
- public AirQualityTime getTime() {
- return time;
+ public Optional<AirQualityTime> getTime() {
+ return Optional.ofNullable(time);
}
/**
*
* @return {AirQualityJsonCity}
*/
- public AirQualityCity getCity() {
- return city;
+ public Optional<AirQualityCity> getCity() {
+ return Optional.ofNullable(city);
}
/**
package org.openhab.binding.airquality.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.annotations.SerializedName;
+import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AirQualityResponse} is the Java class used to map the JSON
* @author Kuba Wolanin - Initial contribution
*/
@NonNullByDefault
-public class AirQualityResponse {
+public class AirQualityResponse extends ResponseRoot {
- public static enum ResponseStatus {
- NONE,
- @SerializedName("error")
- ERROR,
- @SerializedName("ok")
- OK;
- }
+ private @Nullable AirQualityData data;
- private ResponseStatus status = ResponseStatus.NONE;
- private @NonNullByDefault({}) AirQualityData data;
+ public @Nullable AirQualityData getData() {
+ return data;
+ }
- public ResponseStatus getStatus() {
- return status;
+ private ResponseStatus getStatus() {
+ AirQualityData localData = data;
+ return status == ResponseStatus.OK && localData != null && localData.status == ResponseStatus.OK
+ ? ResponseStatus.OK
+ : ResponseStatus.ERROR;
}
- public AirQualityData getData() {
- return data;
+ 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 "";
}
}
--- /dev/null
+/**
+ * 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;
+}
PointType location = provider.getLocation();
AirQualityBridgeHandler bridge = this.bridgeHandler;
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;
}
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);
thingDiscovered(DiscoveryResultBuilder.create(localAirQualityThing).withLabel("Local Air Quality")
.withProperty(LOCATION, String.format("%s,%s", location.getLatitude(), location.getLongitude()))
package org.openhab.binding.airquality.internal.handler;
import java.util.Collection;
-import java.util.Collections;
+import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
- return Collections.singleton(AirQualityDiscoveryService.class);
+ return Set.of(AirQualityDiscoveryService.class);
}
public LocationProvider getLocationProvider() {
private void discoverAttributes() {
getAirQualityData().ifPresent(data -> {
// Update thing properties
- Map<String, String> properties = new HashMap<>();
- properties.put(ATTRIBUTIONS, data.getAttributions());
+ Map<String, String> properties = new HashMap<>(Map.of(ATTRIBUTIONS, data.getAttributions()));
PointType serverLocation = locationProvider.getLocation();
if (serverLocation != null) {
- PointType stationLocation = new PointType(data.getCity().getGeo());
- double distance = serverLocation.distanceFrom(stationLocation).doubleValue();
- properties.put(DISTANCE, new QuantityType<>(distance / 1000, KILO(SIUnits.METRE)).toString());
+ data.getCity().ifPresent(city -> {
+ double distance = serverLocation.distanceFrom(new PointType(city.getGeo())).doubleValue();
+ properties.put(DISTANCE, new QuantityType<>(distance / 1000, KILO(SIUnits.METRE)).toString());
+ });
}
// Search and remove missing pollutant channels
config.put(AirQualityConfiguration.STATION_ID, data.getStationId());
ThingBuilder thingBuilder = editThing();
- thingBuilder.withChannels(channels).withConfiguration(config).withProperties(properties)
- .withLocation(data.getCity().getName());
+ thingBuilder.withChannels(channels).withConfiguration(config).withProperties(properties);
+ data.getCity().map(city -> thingBuilder.withLocation(city.getName()));
updateThing(thingBuilder.build());
});
}
if (apiBridge != null) {
AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
try {
- result = apiBridge.getData(config.stationId, config.location, 0);
+ result = apiBridge.getData(config.stationId, config.location);
updateStatus(ThingStatus.ONLINE);
} catch (AirQualityException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
switch (channelId) {
case TEMPERATURE:
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:
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:
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:
- return new DateTimeType(
- data.getTime().getObservationTime().withZoneSameLocal(timeZoneProvider.getTimeZone()));
+ return data.getTime()
+ .map(time -> (State) new DateTimeType(
+ time.getObservationTime().withZoneSameLocal(timeZoneProvider.getTimeZone())))
+ .orElse(UnDefType.NULL);
case DOMINENT:
return new StringType(data.getDominentPol());
case DEW_POINT:
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:
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:
if (groupId != null) {
double idx = -1;
}
return indexedValue(channelId, idx, pollutant);
}
- return UnDefType.UNDEF;
+ return UnDefType.NULL;
}
}