diff --git a/bundles/org.openhab.binding.airparif/NOTICE b/bundles/org.openhab.binding.airparif/NOTICE index 38d625e3492..a66ce2b105f 100755 --- a/bundles/org.openhab.binding.airparif/NOTICE +++ b/bundles/org.openhab.binding.airparif/NOTICE @@ -11,3 +11,17 @@ https://www.eclipse.org/legal/epl-2.0/. == Source Code https://github.com/openhab/openhab-addons + +== Credits +Icon set coming from the noon project: +Hazel, Ash : Imogen Oh +Birch, Oak : monkik +Cypress, Alder : Levi +Poplar, Rumex : Laymik +Willow : PizzaOter +Hornbeam, Linden : Cannavale +Olive : BnB Studio +Chestnut : Muhammad Fadli Rusady +Plantain : Lars Meiertoberens +Grasses : Neneng Yuliani Lestari +Ragweed, Wormwood : bsd studio diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java index 6ed929d4c87..b56b567303a 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java @@ -33,8 +33,18 @@ public class AirParifBindingConstants { // List of Things Type UIDs public static final ThingTypeUID LOCATION_THING_TYPE = new ThingTypeUID(BINDING_ID, "location"); + // Channel group ids + public static final String GROUP_POLLENS = "pollens"; + public static final String GROUP_DAILY = "daily"; + public static final String GROUP_AQ_BULLETIN = "aq-bulletin"; + public static final String GROUP_AQ_BULLETIN_TOMORROW = GROUP_AQ_BULLETIN + "-tomorrow"; + // List of all Channel ids - public static final String CHANNEL_1 = "channel1"; + public static final String CHANNEL_BEGIN_VALIDITY = "begin-validity"; + public static final String CHANNEL_END_VALIDITY = "end-validity"; + public static final String CHANNEL_COMMENT = "comment"; + public static final String CHANNEL_MESSAGE = "message"; + public static final String CHANNEL_TOMORROW = "tomorrow"; public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE, LOCATION_THING_TYPE); diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifHandlerFactory.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifHandlerFactory.java index ea9f49da607..ac814ef3a5c 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifHandlerFactory.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifHandlerFactory.java @@ -41,12 +41,14 @@ import org.osgi.service.component.annotations.Reference; public class AirParifHandlerFactory extends BaseThingHandlerFactory { private final AirParifDeserializer deserializer; private final HttpClient httpClient; + private final AirParifIconProvider iconProvider; @Activate public AirParifHandlerFactory(final @Reference HttpClientFactory httpClientFactory, - final @Reference AirParifDeserializer deserializer) { + final @Reference AirParifDeserializer deserializer, final @Reference AirParifIconProvider iconProvider) { this.httpClient = httpClientFactory.getCommonHttpClient(); this.deserializer = deserializer; + this.iconProvider = iconProvider; } @Override @@ -59,7 +61,7 @@ public class AirParifHandlerFactory extends BaseThingHandlerFactory { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); return APIBRIDGE_THING_TYPE.equals(thingTypeUID) - ? new AirParifBridgeHandler((Bridge) thing, httpClient, deserializer) + ? new AirParifBridgeHandler((Bridge) thing, httpClient, deserializer, iconProvider) : LOCATION_THING_TYPE.equals(thingTypeUID) ? new LocationHandler(thing) : null; } } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifIconProvider.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifIconProvider.java index 7a9007f2625..d464e0eb9f4 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifIconProvider.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifIconProvider.java @@ -25,6 +25,9 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen; +import org.openhab.binding.airparif.internal.api.ColorMap; +import org.openhab.binding.airparif.internal.api.PollenAlertLevel; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.ui.icon.IconProvider; import org.openhab.core.ui.icon.IconSet; @@ -45,13 +48,17 @@ import org.slf4j.LoggerFactory; @Component(service = { IconProvider.class, AirParifIconProvider.class }) @NonNullByDefault public class AirParifIconProvider implements IconProvider { + private static final String NEUTRAL_COLOR = "#3d3c3c"; private static final String DEFAULT_LABEL = "Air Parif Icons"; private static final String DEFAULT_DESCRIPTION = "Icons illustrating air quality levels provided by AirParif"; private static final List ICONS = List.of("average", "bad", "degrated", "extremely-bad", "good", "pollen"); + private static final List POLLEN_ICONS = Pollen.AS_SET.stream().map(Pollen::name).map(String::toLowerCase) + .toList(); private final Logger logger = LoggerFactory.getLogger(AirParifIconProvider.class); private final TranslationProvider i18nProvider; private final Bundle bundle; + private @Nullable ColorMap colorMap; @Activate public AirParifIconProvider(final BundleContext context, final @Reference TranslationProvider i18nProvider) { @@ -79,7 +86,8 @@ public class AirParifIconProvider implements IconProvider { @Override public @Nullable Integer hasIcon(String category, String iconSetId, Format format) { - return Format.SVG.equals(format) && iconSetId.equals(BINDING_ID) && ICONS.contains(category) ? 0 : null; + return Format.SVG.equals(format) && iconSetId.equals(BINDING_ID) + && (ICONS.contains(category) || POLLEN_ICONS.contains(category)) ? 0 : null; } @Override @@ -89,6 +97,16 @@ public class AirParifIconProvider implements IconProvider { String result; try (InputStream stream = iconResource.openStream()) { result = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + + if (POLLEN_ICONS.contains(category) && state != null) { + try { + int ordinal = Integer.valueOf(state); + PollenAlertLevel alertLevel = PollenAlertLevel.AS_SET.stream() + .filter(pal -> pal.riskLevel == ordinal).findFirst().orElse(PollenAlertLevel.UNKNOWN); + result = result.replaceAll(NEUTRAL_COLOR, alertLevel.color); + } catch (NumberFormatException ignore) { + } + } } catch (IOException e) { logger.warn("Unable to load ressource '{}': {}", iconResource.getPath(), e.getMessage()); result = ""; @@ -96,4 +114,8 @@ public class AirParifIconProvider implements IconProvider { return result.isEmpty() ? null : new ByteArrayInputStream(result.getBytes()); } + + public void setColorMap(ColorMap map) { + this.colorMap = map; + } } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java index bb2238f8692..ef9ad505fb4 100644 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java @@ -13,11 +13,15 @@ package org.openhab.binding.airparif.internal.api; import java.time.LocalDate; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -45,7 +49,7 @@ public class AirParifDto { @SerializedName("droits") Set scopes) { } - private record Message(// + public record Message(// String fr, // @Nullable String en) { } @@ -92,36 +96,73 @@ public class AirParifDto { Map valeurs, // String commentaire, // String periode) { + } + public class PollensResponse { private static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yy"); private static Pattern PATTERN = Pattern.compile("\\d{2}.\\d{2}.\\d{2}"); + private static ZoneId DEFAULT_ZONE = ZoneId.of("Europe/Paris"); - private static @Nullable LocalDate getValidity(String periode, boolean begin) { - Matcher matcher = PATTERN.matcher(periode); - if (matcher.find()) { - String extractedDate = matcher.group(); - if (begin) { - return LocalDate.parse(extractedDate, FORMATTER); + public List data = List.of(); + + public Optional getData() { + return Optional.ofNullable(data.isEmpty() ? null : data.get(0)); + } + + private Set getValidities() { + Set result = new TreeSet<>(); + getData().ifPresent(pollens -> { + Matcher matcher = PATTERN.matcher(pollens.periode); + while (matcher.find()) { + result.add(LocalDate.parse(matcher.group(), FORMATTER).atStartOfDay(DEFAULT_ZONE)); } - if (matcher.find()) { - extractedDate = matcher.group(); - return LocalDate.parse(extractedDate, FORMATTER); + }); + return result; + } + + public Optional getBeginValidity() { + return Optional.ofNullable(getValidities().iterator().next()); + } + + public Optional getEndValidity() { + return Optional.ofNullable(getValidities().stream().reduce((prev, next) -> next).orElse(null)); + } + + public Optional getComment() { + return getData().map(pollens -> pollens.commentaire); + } + + public Map getDepartment(String id) { + Map result = new HashMap<>(); + Optional donnees = getData(); + if (donnees.isPresent()) { + Pollens depts = donnees.get(); + PollenAlertLevel[] valeurs = depts.valeurs.get(id); + if (valeurs != null) { + for (int i = 0; i < valeurs.length; i++) { + result.put(depts.taxons[i], valeurs[i]); + } } } - return null; + return result; } + } - public @Nullable LocalDate beginValidity() { - return getValidity(periode, true); - } + public record Result(// + @SerializedName("polluant") Pollutant pollutant, // + ZonedDateTime date, // + @SerializedName("valeurs") double[] values, // + Message message) { + } - public @Nullable LocalDate endValidity() { - return getValidity(periode, false); - } + public record Route(// + @SerializedName("dateRequise") ZonedDateTime requestedDate, // + double[][] longlats, // + @SerializedName("resultats") Result[] results, // + @Nullable Message[] messages) { } - public record PollensResponse(ArrayList data) { + public record ItineraireResponse(@SerializedName("itineraires") Route[] routes) { } - } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/ColorMap.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/ColorMap.java index 59aa08af91d..0c9d95d2764 100644 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/ColorMap.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/ColorMap.java @@ -26,8 +26,8 @@ public class ColorMap extends HashMap { private static final long serialVersionUID = -605462873565278453L; private static Appreciation fromApiName(String searched) { - return Objects.requireNonNull( - Appreciation.AS_SET.stream().filter(mt -> searched.equals(mt.apiName)).findFirst().orElse(Appreciation.UNKNOWN)); + return Objects.requireNonNull(Appreciation.AS_SET.stream().filter(mt -> searched.equals(mt.apiName)).findFirst() + .orElse(Appreciation.UNKNOWN)); } public String put(String key, String value) { diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/PollenAlertLevel.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/PollenAlertLevel.java index 74b2517cded..59b2d821ba3 100644 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/PollenAlertLevel.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/PollenAlertLevel.java @@ -24,20 +24,22 @@ import com.google.gson.annotations.SerializedName; @NonNullByDefault public enum PollenAlertLevel { @SerializedName("0") - NONE(0), + NONE(0, "#3a8b2f"), @SerializedName("1") - LOW(1), + LOW(1, "#f9a825"), @SerializedName("2") - AVERAGE(2), + AVERAGE(2, "#ef6c00"), @SerializedName("3") - HIGH(3), - UNKNOWN(-1); + HIGH(3, "#b71c1c"), + UNKNOWN(-1, "#b3b3b3"); public static final EnumSet AS_SET = EnumSet.allOf(PollenAlertLevel.class); public final int riskLevel; + public final String color; - PollenAlertLevel(int riskLevel) { + PollenAlertLevel(int riskLevel, String color) { this.riskLevel = riskLevel; + this.color = color; } } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/Pollutant.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/Pollutant.java index 21a1163a90b..4298a621aad 100644 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/Pollutant.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/Pollutant.java @@ -14,7 +14,12 @@ package org.openhab.binding.airparif.internal.api; import java.util.EnumSet; +import javax.measure.Unit; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.unit.Units; + +import com.google.gson.annotations.SerializedName; /** * The {@link Pollutant} enum lists all pollutants tracked by AirParif @@ -23,14 +28,31 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public enum Pollutant { - PM25, - PM10, - NO2, - O3, - UNKNOWN; + @SerializedName("pm25") + PM25(Units.MICROGRAM_PER_CUBICMETRE), + + @SerializedName("pm10") + PM10(Units.MICROGRAM_PER_CUBICMETRE), + + @SerializedName("no2") + NO2(Units.PARTS_PER_BILLION), + + @SerializedName("o3") + O3(Units.PARTS_PER_BILLION), + + @SerializedName("indice") + INDICE(Units.PERCENT), + + UNKNOWN(Units.PERCENT); public static final EnumSet AS_SET = EnumSet.allOf(Pollutant.class); + public final Unit unit; + + Pollutant(Unit unit) { + this.unit = unit; + } + public static Pollutant safeValueOf(String searched) { try { return Pollutant.valueOf(searched); diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/LocationConfiguration.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/LocationConfiguration.java index 0053cf0f9f5..3c7f8242997 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/LocationConfiguration.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/LocationConfiguration.java @@ -23,7 +23,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @NonNullByDefault public class LocationConfiguration { public static final String LOCATION = "location"; + public static final String DEPARTMENT = "department"; public int refresh = 10; public String location = ""; + public String department = ""; } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java index b9278becd4f..851ef94c040 100644 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java @@ -24,8 +24,6 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.types.PointType; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +39,6 @@ import com.google.gson.JsonSyntaxException; * @author Gaƫl L'hopital - Initial Contribution */ -@Component(service = DepartmentDbService.class) @NonNullByDefault public class DepartmentDbService { private final Logger logger = LoggerFactory.getLogger(DepartmentDbService.class); @@ -51,7 +48,6 @@ public class DepartmentDbService { double westestLon) { } - @Activate public DepartmentDbService() { try (InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("/db/departments.json"); diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/AirParifDeserializer.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/AirParifDeserializer.java index a96b359ea00..a66b32a87fc 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/AirParifDeserializer.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/AirParifDeserializer.java @@ -44,7 +44,7 @@ public class AirParifDeserializer { @Activate public AirParifDeserializer(final @Reference TimeZoneProvider timeZoneProvider) { - gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) .registerTypeAdapter(PollenAlertLevel.class, new PollenAlertLevelDeserializer()) .registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory()) .registerTypeAdapter(ColorMap.class, new ColorMapDeserializer()) @@ -52,11 +52,11 @@ public class AirParifDeserializer { .registerTypeAdapter(LocalDate.class, (JsonDeserializer) (json, type, context) -> LocalDate .parse(json.getAsJsonPrimitive().getAsString())) - .registerTypeAdapter(ZonedDateTime.class, - (JsonDeserializer) (json, type, context) -> ZonedDateTime - .parse(json.getAsJsonPrimitive().getAsString() + "Z") - .withZoneSameInstant(timeZoneProvider.getTimeZone())) - .create(); + .registerTypeAdapter(ZonedDateTime.class, (JsonDeserializer) (json, type, context) -> { + String string = json.getAsJsonPrimitive().getAsString(); + string += string.contains("+") ? "" : "Z"; + return ZonedDateTime.parse(string).withZoneSameInstant(timeZoneProvider.getTimeZone()); + }).create(); } public T deserialize(Class clazz, String json) throws AirParifException { @@ -68,8 +68,7 @@ public class AirParifDeserializer { } throw new AirParifException("Deserialization of '%s' resulted in null value", json); } catch (JsonSyntaxException e) { - throw new AirParifException(e, "Unexpected error deserializing '%s'", json); + throw new AirParifException(e, "Unexpected error deserializing '%s'", e.getMessage()); } } - } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java index 112796875e2..42bcfbb6bc5 100644 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java @@ -39,9 +39,8 @@ class PollenAlertLevelDeserializer implements JsonDeserializer } catch (JsonSyntaxException ignore) { return PollenAlertLevel.UNKNOWN; } - + return PollenAlertLevel.AS_SET.stream().filter(s -> s.riskLevel == level).findFirst() .orElse(PollenAlertLevel.UNKNOWN); - } } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/discovery/AirParifDiscoveryService.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/discovery/AirParifDiscoveryService.java index 6df88a8a2d1..61741ff9f7b 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/discovery/AirParifDiscoveryService.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/discovery/AirParifDiscoveryService.java @@ -13,15 +13,17 @@ package org.openhab.binding.airparif.internal.discovery; import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*; -import static org.openhab.binding.airparif.internal.config.LocationConfiguration.LOCATION; + +import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.airparif.internal.config.LocationConfiguration; +import org.openhab.binding.airparif.internal.db.DepartmentDbService; +import org.openhab.binding.airparif.internal.db.DepartmentDbService.Department; import org.openhab.binding.airparif.internal.handler.AirParifBridgeHandler; import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; -import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocationProvider; -import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.PointType; import org.openhab.core.thing.ThingUID; import org.osgi.service.component.annotations.Component; @@ -41,40 +43,47 @@ public class AirParifDiscoveryService extends AbstractThingHandlerDiscoveryServi private static final int DISCOVER_TIMEOUT_SECONDS = 2; private final Logger logger = LoggerFactory.getLogger(AirParifDiscoveryService.class); + private final DepartmentDbService dbService; private @NonNullByDefault({}) LocationProvider locationProvider; public AirParifDiscoveryService() { super(AirParifBridgeHandler.class, SUPPORTED_THING_TYPES_UIDS, DISCOVER_TIMEOUT_SECONDS); + dbService = new DepartmentDbService(); } @Reference(unbind = "-") - public void bindTranslationProvider(TranslationProvider translationProvider) { - this.i18nProvider = translationProvider; - } - - @Reference(unbind = "-") - public void bindLocaleProvider(LocaleProvider localeProvider) { - this.localeProvider = localeProvider; - } - - @Reference(unbind = "-") - public void bindLocationProvider(LocationProvider locationProvider) { + public void setLocationProvider(LocationProvider locationProvider) { this.locationProvider = locationProvider; } @Override - protected void startScan() { + public void startScan() { logger.debug("Starting AirParif discovery scan"); - if (locationProvider.getLocation() instanceof PointType location) { - ThingUID bridgeUID = thingHandler.getThing().getUID(); - thingDiscovered(DiscoveryResultBuilder.create(new ThingUID(LOCATION_THING_TYPE, bridgeUID, LOCAL)) - .withLabel("@text/discovery.airparif.location.local.label") // - .withProperty(LOCATION, location.toString()) // - .withRepresentationProperty(LOCATION) // - .withBridge(bridgeUID).build()); + + LocationProvider localLocation = locationProvider; + PointType location = localLocation != null ? localLocation.getLocation() : null; + if (location == null) { + logger.debug("LocationProvider.getLocation() is not set -> Will not provide any discovery results"); + return; + } + + createDepartmentResults(location); + } + + private void createDepartmentResults(PointType serverLocation) { + List candidates = dbService.getBounding(serverLocation); + ThingUID bridgeUID = thingHandler.getThing().getUID(); + if (!candidates.isEmpty()) { + candidates.forEach(dep -> thingDiscovered( + DiscoveryResultBuilder.create(new ThingUID(LOCATION_THING_TYPE, bridgeUID, dep.id()))// + .withLabel("Location Report: %s".formatted(dep.name())) // + .withProperty(LocationConfiguration.DEPARTMENT, dep.id()) // + .withProperty(LocationConfiguration.LOCATION, serverLocation.toFullString())// + .withRepresentationProperty(LocationConfiguration.DEPARTMENT) // + .withBridge(bridgeUID).build())); } else { - logger.debug("LocationProvider.getLocation() is not set, no discovery results can be provided"); + logger.info("No department could be discovered matching server location"); } } } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java index bb2883ee288..c3db0bb8c95 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java @@ -12,13 +12,22 @@ */ package org.openhab.binding.airparif.internal.handler; +import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*; import static org.openhab.binding.airparif.internal.api.AirParifApi.*; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -26,29 +35,38 @@ import java.util.stream.Collectors; import javax.ws.rs.core.MediaType; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.InputStreamContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus.Code; import org.openhab.binding.airparif.internal.AirParifException; +import org.openhab.binding.airparif.internal.AirParifIconProvider; import org.openhab.binding.airparif.internal.api.AirParifDto.Bulletin; import org.openhab.binding.airparif.internal.api.AirParifDto.Episode; +import org.openhab.binding.airparif.internal.api.AirParifDto.ItineraireResponse; import org.openhab.binding.airparif.internal.api.AirParifDto.KeyInfo; -import org.openhab.binding.airparif.internal.api.AirParifDto.Pollens; import org.openhab.binding.airparif.internal.api.AirParifDto.PollensResponse; +import org.openhab.binding.airparif.internal.api.AirParifDto.Route; import org.openhab.binding.airparif.internal.api.AirParifDto.Version; import org.openhab.binding.airparif.internal.api.ColorMap; import org.openhab.binding.airparif.internal.config.BridgeConfiguration; import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; 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.BaseBridgeHandler; import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,13 +84,19 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(AirParifBridgeHandler.class); private final AirParifDeserializer deserializer; + private final AirParifIconProvider iconProvider; private final HttpClient httpClient; private BridgeConfiguration config = new BridgeConfiguration(); - public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer) { + private @Nullable ScheduledFuture pollensJob; + private @Nullable ScheduledFuture dailyJob; + + public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer, + AirParifIconProvider iconProvider) { super(bridge); this.deserializer = deserializer; + this.iconProvider = iconProvider; this.httpClient = httpClient; } @@ -85,16 +109,39 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { "@text/offline.config-error-unknown-apikey"); return; } - initiateConnexion(); + scheduler.execute(this::initiateConnexion); } - public synchronized String executeUri(URI uri) throws AirParifException { + private @Nullable ScheduledFuture cancelFuture(@Nullable ScheduledFuture job) { + if (job != null && !job.isCancelled()) { + job.cancel(true); + } + return null; + } + + @Override + public void dispose() { + logger.debug("Disposing the AirParif bridge handler."); + + pollensJob = cancelFuture(pollensJob); + dailyJob = cancelFuture(dailyJob); + } + + public synchronized String executeUri(URI uri, HttpMethod method, @Nullable String payload) + throws AirParifException { logger.debug("executeUrl: {} ", uri); - Request request = httpClient.newRequest(uri).method(HttpMethod.GET) - .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) + Request request = httpClient.newRequest(uri).method(method).timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) .header(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON).header("X-Api-Key", config.apikey); + if (payload != null && HttpMethod.POST.equals(method)) { + InputStream stream = new ByteArrayInputStream(payload.getBytes(DEFAULT_CHARSET)); + try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) { + request.content(inputStreamContentProvider, MediaType.APPLICATION_JSON); + } + logger.trace(" -with payload : {} ", payload); + } + try { ContentResponse response = request.send(); @@ -107,7 +154,7 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { } else if (statusCode == Code.FORBIDDEN) { throw new AirParifException("@text/offline.config-error-invalid-apikey"); } - + String content = new String(response.getContent(), DEFAULT_CHARSET); throw new AirParifException("Error '%s' requesting: %s", statusCode.getMessage(), uri.toString()); } catch (TimeoutException | ExecutionException e) { throw new AirParifException(e, "Exception while calling %s", request.getURI()); @@ -117,7 +164,12 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { } public synchronized T executeUri(URI uri, Class clazz) throws AirParifException { - String content = executeUri(uri); + String content = executeUri(uri, HttpMethod.GET, null); + return deserializer.deserialize(clazz, content); + } + + public synchronized T executeUri(URI uri, Class clazz, String payload) throws AirParifException { + String content = executeUri(uri, HttpMethod.POST, payload); return deserializer.deserialize(clazz, content); } @@ -128,8 +180,6 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { private void initiateConnexion() { Version version; - KeyInfo keyInfo; - try { // This does validate communication with the server version = executeUri(VERSION_URI, Version.class); } catch (AirParifException e) { @@ -137,6 +187,7 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { return; } + KeyInfo keyInfo; try { // This validates the api key value keyInfo = executeUri(KEY_INFO_URI, KeyInfo.class); } catch (AirParifException e) { @@ -146,31 +197,101 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { getThing().setProperty("api-version", version.version()); getThing().setProperty("key-expiration", keyInfo.expiration().toString()); - logger.info("The api key is valid until {}", keyInfo.expiration().toString()); getThing().setProperty("scopes", keyInfo.scopes().stream().map(e -> e.name()).collect(Collectors.joining(","))); + logger.info("The api key is valid until {}", keyInfo.expiration().toString()); updateStatus(ThingStatus.ONLINE); try { ColorMap map = executeUri(PREV_COLORS_URI, ColorMap.class); - logger.info("The color map is {}", map.toString()); + logger.debug("The color map is {}", map.toString()); + iconProvider.setColorMap(map); + } catch (AirParifException e) { + logger.warn("Error reading ColorMap: {]", e.getMessage()); + } + pollensJob = scheduler.schedule(this::updatePollens, 1, TimeUnit.SECONDS); + dailyJob = scheduler.schedule(this::updateDaily, 2, TimeUnit.SECONDS); + } + + private void updateDaily() { + try { Bulletin bulletin = executeUri(PREV_BULLETIN_URI, Bulletin.class); - logger.info("The bulletin is {}", bulletin.today().dayDescription()); + logger.debug("The bulletin is {}", bulletin.today().dayDescription()); + + Set.of(bulletin.today(), bulletin.tomorrow()).stream().forEach(aq -> { + String groupName = aq.previsionDate().equals(LocalDate.now()) ? GROUP_AQ_BULLETIN + : GROUP_AQ_BULLETIN_TOMORROW + "#"; + updateState(groupName + CHANNEL_COMMENT, + !aq.available() ? UnDefType.UNDEF : new StringType(aq.bulletin().fr())); + aq.concentrations().forEach(measure -> { + String cName = groupName + measure.pollutant().name().toLowerCase(); + updateState(cName + "-min", !aq.available() ? UnDefType.UNDEF + : new QuantityType<>(measure.min(), measure.pollutant().unit)); + updateState(cName + "-max", !aq.available() ? UnDefType.UNDEF + : new QuantityType<>(measure.max(), measure.pollutant().unit)); + }); + }); Episode episode = executeUri(EPISODES_URI, Episode.class); - logger.info("The bulletin is {}", episode); + logger.debug("The episode is {}", episode); - Pollens pollens = executeUri(POLLENS_URI, PollensResponse.class).data().get(0); - logger.info("The pollens are {}", pollens); - LocalDate begin = pollens.beginValidity(); - LocalDate end = pollens.endValidity(); + // if (episode.active()) { + // updateState(GROUP_DAILY + "#" + CHANNEL_MESSAGE, new StringType(episode.message().fr())); + // updateState(GROUP_DAILY + "#" + CHANNEL_TOMORROW, new StringType(episode.message().fr())); + // } - String response = executeUri(POLLENS_DEPT_BUILDER.path("78").build()); - logger.info("The pollens 78 {}", response); + ZonedDateTime tomorrowMorning = ZonedDateTime.now().plusDays(1).truncatedTo(ChronoUnit.DAYS).plusMinutes(1); + long delay = Duration.between(ZonedDateTime.now(), tomorrowMorning).getSeconds(); + logger.debug("Rescheduling daily job tomorrow morning"); + dailyJob = scheduler.schedule(this::updateDaily, delay, TimeUnit.SECONDS); } catch (AirParifException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); - return; + logger.warn("Error update pollens data: {}", e.getMessage()); } } + private void updatePollens() { + try { + PollensResponse pollens = executeUri(POLLENS_URI, PollensResponse.class); + + pollens.getComment() + .ifPresent(comment -> updateState(GROUP_POLLENS + "#" + CHANNEL_COMMENT, new StringType(comment))); + pollens.getBeginValidity().ifPresent( + begin -> updateState(GROUP_POLLENS + "#" + CHANNEL_BEGIN_VALIDITY, new DateTimeType(begin))); + pollens.getEndValidity().ifPresent(end -> { + updateState(GROUP_POLLENS + "#" + CHANNEL_END_VALIDITY, new DateTimeType(end)); + logger.info("Pollens bulletin valid until {}", end); + long delay = Duration.between(ZonedDateTime.now(), end).getSeconds(); + if (delay < 0) { + // what if the bulletin was not updated and the delay is passed ? + delay = 3600; + logger.debug("Update time of the bulletin is in the past - will retry in one hour"); + } else { + delay += 60; + } + + pollensJob = scheduler.schedule(this::updatePollens, delay, TimeUnit.SECONDS); + }); + getThing().getThings().stream().map(Thing::getHandler).filter(LocationHandler.class::isInstance) + .map(LocationHandler.class::cast).forEach(locHand -> locHand.setPollens(pollens)); + } catch (AirParifException e) { + logger.warn("Error updating pollens data: {}", e.getMessage()); + } + } + + public @Nullable Route getConcentrations(String location) { + String[] elements = location.split(","); + if (elements.length >= 2) { + String req = "{\"itineraires\": [{\"date\": \"%s\",\"longlats\": [[%s,%s]]}],\"polluants\": [\"indice\",\"no2\",\"o3\",\"pm25\",\"pm10\"]}"; + req = req.formatted(LocalDateTime.now().truncatedTo(ChronoUnit.HOURS), elements[1], elements[0]); + try { + ItineraireResponse result = executeUri(HORAIR_URI, ItineraireResponse.class, req); + return result.routes()[0]; + } catch (AirParifException e) { + logger.warn("Error getting detailed concentrations: {}", e.getMessage()); + } + } else { + logger.warn("Wrong localisation as input : {}", location); + } + return null; + } } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java index 11450b1ecb2..55f8484dfd8 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java @@ -12,17 +12,25 @@ */ package org.openhab.binding.airparif.internal.handler; -import static org.openhab.binding.airparif.internal.AirParifBindingConstants.CHANNEL_1; +import static org.openhab.binding.airparif.internal.AirParifBindingConstants.GROUP_POLLENS; + +import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen; +import org.openhab.binding.airparif.internal.api.AirParifDto.PollensResponse; +import org.openhab.binding.airparif.internal.api.AirParifDto.Route; +import org.openhab.binding.airparif.internal.api.PollenAlertLevel; import org.openhab.binding.airparif.internal.config.LocationConfiguration; +import org.openhab.core.library.types.DecimalType; +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.types.Command; -import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,36 +51,50 @@ public class LocationHandler extends BaseThingHandler { super(thing); } - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - if (CHANNEL_1.equals(channelUID.getId())) { - if (command instanceof RefreshType) { - // TODO: handle data refresh - } - - // TODO: handle command - - // Note: if communication with thing fails for some reason, - // indicate that by setting the status with detail information: - // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - // "Could not control device at IP address x.x.x.x"); - } - } - @Override public void initialize() { config = getConfigAs(LocationConfiguration.class); updateStatus(ThingStatus.UNKNOWN); - // Example for background initialization: - scheduler.execute(() -> { - boolean thingReachable = true; // - // when done do: - if (thingReachable) { - updateStatus(ThingStatus.ONLINE); + scheduler.execute(this::getConcentrations); + } + + public void setPollens(PollensResponse pollens) { + LocationConfiguration local = config; + if (local != null) { + Map alerts = pollens.getDepartment(local.department); + alerts.forEach((pollen, level) -> { + updateState(GROUP_POLLENS + "#" + pollen.name().toLowerCase(), new DecimalType(level.ordinal())); + }); + updateStatus(ThingStatus.ONLINE); + } + } + + private void getConcentrations() { + AirParifBridgeHandler apiHandler = getApiBridgeHandler(); + LocationConfiguration local = config; + if (apiHandler != null && local != null) { + Route route = apiHandler.getConcentrations(local.location); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // TODO Auto-generated method stub + + } + + private @Nullable AirParifBridgeHandler getApiBridgeHandler() { + Bridge bridge = this.getBridge(); + if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) { + if (bridge.getHandler() instanceof AirParifBridgeHandler airParifBridgeHandler) { + return airParifBridgeHandler; } else { - updateStatus(ThingStatus.OFFLINE); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge"); } - }); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + return null; } } diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/addon/addon.xml index 74286c6bb4c..c6016d94731 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/addon/addon.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/addon/addon.xml @@ -6,7 +6,7 @@ binding AirParif Binding Air Quality data and forecasts provided by AirParif. - cloud - fr - + cloud + fr + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties index 98191f8a6c2..448b12d4d94 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties @@ -1,3 +1,225 @@ +# add-on + +addon.airparif.name = AirParif Binding +addon.airparif.description = Air Quality data and forecasts provided by AirParif. + +# thing types + +thing-type.airparif.api.label = AirParif API Portal +thing-type.airparif.api.description = Bridge to the AirParif API Portal. In order to receive the data, you must register an account on https://www.airparif.fr/contact and receive your API token. +thing-type.airparif.location.label = Department Report +thing-type.airparif.location.description = AirParif air quality report for the given location + +# thing types config + +thing-type.config.airparif.api.apikey.label = API Key +thing-type.config.airparif.api.apikey.description = Token used to access the service +thing-type.config.airparif.location.department.label = Department +thing-type.config.airparif.location.department.description = Name of the department +thing-type.config.airparif.location.department.option.75 = Paris +thing-type.config.airparif.location.department.option.77 = Seine et Marne +thing-type.config.airparif.location.department.option.78 = Yvelines +thing-type.config.airparif.location.department.option.91 = Essonne +thing-type.config.airparif.location.department.option.92 = Hauts de Seine +thing-type.config.airparif.location.department.option.93 = Seine Saint Denis +thing-type.config.airparif.location.department.option.94 = Val de Marne +thing-type.config.airparif.location.department.option.95 = Val D'Oise +thing-type.config.airparif.location.location.label = Location + +# channel group types + +channel-group-type.airparif.bridge-pollens.label = Pollen information for the region +channel-group-type.airparif.bridge-pollens.channel.begin-validity.label = Begin Validity +channel-group-type.airparif.bridge-pollens.channel.begin-validity.description = Current bulletin validity start +channel-group-type.airparif.bridge-pollens.channel.comment.label = Begin Validity +channel-group-type.airparif.bridge-pollens.channel.comment.description = Current bulletin validity start +channel-group-type.airparif.bridge-pollens.channel.end-validity.label = End Validity +channel-group-type.airparif.bridge-pollens.channel.end-validity.description = Current bulletin validity ending +channel-group-type.airparif.dept-pollens.label = Pollen information for the department + +# channel types + +channel-type.airparif.alder-level.label = Alder +channel-type.airparif.alder-level.state.option.0 = None +channel-type.airparif.alder-level.state.option.1 = Low +channel-type.airparif.alder-level.state.option.2 = Average +channel-type.airparif.alder-level.state.option.3 = High +channel-type.airparif.ash-level.label = Ash +channel-type.airparif.ash-level.state.option.0 = None +channel-type.airparif.ash-level.state.option.1 = Low +channel-type.airparif.ash-level.state.option.2 = Average +channel-type.airparif.ash-level.state.option.3 = High +channel-type.airparif.birch-level.label = Birch Level +channel-type.airparif.birch-level.state.option.0 = None +channel-type.airparif.birch-level.state.option.1 = Low +channel-type.airparif.birch-level.state.option.2 = Average +channel-type.airparif.birch-level.state.option.3 = High +channel-type.airparif.chestnut-level.label = Chestnut +channel-type.airparif.chestnut-level.state.option.0 = None +channel-type.airparif.chestnut-level.state.option.1 = Low +channel-type.airparif.chestnut-level.state.option.2 = Average +channel-type.airparif.chestnut-level.state.option.3 = High +channel-type.airparif.comment.label = Comment +channel-type.airparif.cypress-level.label = Cypress +channel-type.airparif.cypress-level.state.option.0 = None +channel-type.airparif.cypress-level.state.option.1 = Low +channel-type.airparif.cypress-level.state.option.2 = Average +channel-type.airparif.cypress-level.state.option.3 = High +channel-type.airparif.grasses-level.label = Grasses +channel-type.airparif.grasses-level.state.option.0 = None +channel-type.airparif.grasses-level.state.option.1 = Low +channel-type.airparif.grasses-level.state.option.2 = Average +channel-type.airparif.grasses-level.state.option.3 = High +channel-type.airparif.hazel-level.label = Hazel Level +channel-type.airparif.hazel-level.state.option.0 = None +channel-type.airparif.hazel-level.state.option.1 = Low +channel-type.airparif.hazel-level.state.option.2 = Average +channel-type.airparif.hazel-level.state.option.3 = High +channel-type.airparif.hornbeam-level.label = Hornbeam +channel-type.airparif.hornbeam-level.state.option.0 = None +channel-type.airparif.hornbeam-level.state.option.1 = Low +channel-type.airparif.hornbeam-level.state.option.2 = Average +channel-type.airparif.hornbeam-level.state.option.3 = High +channel-type.airparif.linden-level.label = Linden +channel-type.airparif.linden-level.state.option.0 = None +channel-type.airparif.linden-level.state.option.1 = Low +channel-type.airparif.linden-level.state.option.2 = Average +channel-type.airparif.linden-level.state.option.3 = High +channel-type.airparif.oak-level.label = Oak +channel-type.airparif.oak-level.state.option.0 = None +channel-type.airparif.oak-level.state.option.1 = Low +channel-type.airparif.oak-level.state.option.2 = Average +channel-type.airparif.oak-level.state.option.3 = High +channel-type.airparif.olive-level.label = Olive +channel-type.airparif.olive-level.state.option.0 = None +channel-type.airparif.olive-level.state.option.1 = Low +channel-type.airparif.olive-level.state.option.2 = Average +channel-type.airparif.olive-level.state.option.3 = High +channel-type.airparif.plane-level.label = Plane +channel-type.airparif.plane-level.state.option.0 = None +channel-type.airparif.plane-level.state.option.1 = Low +channel-type.airparif.plane-level.state.option.2 = Average +channel-type.airparif.plane-level.state.option.3 = High +channel-type.airparif.plantain-level.label = Plantain +channel-type.airparif.plantain-level.state.option.0 = None +channel-type.airparif.plantain-level.state.option.1 = Low +channel-type.airparif.plantain-level.state.option.2 = Average +channel-type.airparif.plantain-level.state.option.3 = High +channel-type.airparif.poplar-level.label = Poplar +channel-type.airparif.poplar-level.state.option.0 = None +channel-type.airparif.poplar-level.state.option.1 = Low +channel-type.airparif.poplar-level.state.option.2 = Average +channel-type.airparif.poplar-level.state.option.3 = High +channel-type.airparif.ragweed-level.label = Ragweed +channel-type.airparif.ragweed-level.state.option.0 = None +channel-type.airparif.ragweed-level.state.option.1 = Low +channel-type.airparif.ragweed-level.state.option.2 = Average +channel-type.airparif.ragweed-level.state.option.3 = High +channel-type.airparif.rumex-level.label = Rumex +channel-type.airparif.rumex-level.state.option.0 = None +channel-type.airparif.rumex-level.state.option.1 = Low +channel-type.airparif.rumex-level.state.option.2 = Average +channel-type.airparif.rumex-level.state.option.3 = High +channel-type.airparif.timestamp.label = Timestamp +channel-type.airparif.urticaceae-level.label = Urticacea +channel-type.airparif.urticaceae-level.state.option.0 = None +channel-type.airparif.urticaceae-level.state.option.1 = Low +channel-type.airparif.urticaceae-level.state.option.2 = Average +channel-type.airparif.urticaceae-level.state.option.3 = High +channel-type.airparif.willow-level.label = Willow +channel-type.airparif.willow-level.state.option.0 = None +channel-type.airparif.willow-level.state.option.1 = Low +channel-type.airparif.willow-level.state.option.2 = Average +channel-type.airparif.willow-level.state.option.3 = High +channel-type.airparif.wormwood-level.label = Wormwood +channel-type.airparif.wormwood-level.state.option.0 = None +channel-type.airparif.wormwood-level.state.option.1 = Low +channel-type.airparif.wormwood-level.state.option.2 = Average +channel-type.airparif.wormwood-level.state.option.3 = High + +# thing types + +thing-type.airparif.location.channel.end-validity.label = End Of Validity +thing-type.airparif.location.channel.end-validity.description = Current bulletin validity ending + +# channel group types + +channel-group-type.airparif.pollens-group.label = Pollen information for the region +channel-group-type.airparif.pollens-group.channel.begin-validity.label = Begin Validity +channel-group-type.airparif.pollens-group.channel.begin-validity.description = Current bulletin validity start +channel-group-type.airparif.pollens-group.channel.comment.label = Begin Validity +channel-group-type.airparif.pollens-group.channel.comment.description = Current bulletin validity start +channel-group-type.airparif.pollens-group.channel.end-validity.label = End Validity +channel-group-type.airparif.pollens-group.channel.end-validity.description = Current bulletin validity ending + +# channel types + +channel-type.airparif.alert-level.state.option.0 = Good +channel-type.airparif.alert-level.state.option.1 = Average +channel-type.airparif.alert-level.state.option.2 = Degrated +channel-type.airparif.alert-level.state.option.3 = Bad +channel-type.airparif.alert-level.state.option.4 = Extremely Bad +channel-type.airparif.avalanches.label = Avalanches +channel-type.airparif.avalanches.description = Avalanche alert level +channel-type.airparif.avalanches.state.option.0 = No special vigilance +channel-type.airparif.avalanches.state.option.1 = Be attentive +channel-type.airparif.avalanches.state.option.2 = Be very vigilant +channel-type.airparif.avalanches.state.option.3 = Absolute vigilance +channel-type.airparif.canicule.label = Heat Wave +channel-type.airparif.canicule.description = High temperature alert level +channel-type.airparif.canicule.state.option.0 = No special vigilance +channel-type.airparif.canicule.state.option.1 = Be attentive +channel-type.airparif.canicule.state.option.2 = Be very vigilant +channel-type.airparif.canicule.state.option.3 = Absolute vigilance +channel-type.airparif.condition-icon.label = Icon +channel-type.airparif.condition-icon.description = Pictogram associated with the alert level. +channel-type.airparif.grand-froid.label = Extreme Cold +channel-type.airparif.grand-froid.description = Negative temperature alert level +channel-type.airparif.grand-froid.state.option.0 = No special vigilance +channel-type.airparif.grand-froid.state.option.1 = Be attentive +channel-type.airparif.grand-froid.state.option.2 = Be very vigilant +channel-type.airparif.grand-froid.state.option.3 = Absolute vigilance +channel-type.airparif.inondation.label = Flood +channel-type.airparif.inondation.description = Flood alert level +channel-type.airparif.inondation.state.option.0 = No special vigilance +channel-type.airparif.inondation.state.option.1 = Be attentive +channel-type.airparif.inondation.state.option.2 = Be very vigilant +channel-type.airparif.inondation.state.option.3 = Absolute vigilance +channel-type.airparif.neige.label = Snow +channel-type.airparif.neige.description = Snow event alert level +channel-type.airparif.neige.state.option.0 = No special vigilance +channel-type.airparif.neige.state.option.1 = Be attentive +channel-type.airparif.neige.state.option.2 = Be very vigilant +channel-type.airparif.neige.state.option.3 = Absolute vigilance +channel-type.airparif.orage.label = Storm +channel-type.airparif.orage.description = Storm alert level +channel-type.airparif.orage.state.option.0 = No special vigilance +channel-type.airparif.orage.state.option.1 = Be attentive +channel-type.airparif.orage.state.option.2 = Be very vigilant +channel-type.airparif.orage.state.option.3 = Absolute vigilance +channel-type.airparif.pluie-inondation.label = Rain Flood +channel-type.airparif.pluie-inondation.description = Flood caused by rainfall alert level +channel-type.airparif.pluie-inondation.state.option.0 = No special vigilance +channel-type.airparif.pluie-inondation.state.option.1 = Be attentive +channel-type.airparif.pluie-inondation.state.option.2 = Be very vigilant +channel-type.airparif.pluie-inondation.state.option.3 = Absolute vigilance +channel-type.airparif.pollen-level.label = Pollen Level +channel-type.airparif.pollen-level.state.option.0 = None +channel-type.airparif.pollen-level.state.option.1 = Low +channel-type.airparif.pollen-level.state.option.2 = Average +channel-type.airparif.pollen-level.state.option.3 = High +channel-type.airparif.vague-submersion.label = Wave Submersion +channel-type.airparif.vague-submersion.description = Submersion wave alert level +channel-type.airparif.vague-submersion.state.option.0 = No special vigilance +channel-type.airparif.vague-submersion.state.option.1 = Be attentive +channel-type.airparif.vague-submersion.state.option.2 = Be very vigilant +channel-type.airparif.vague-submersion.state.option.3 = Absolute vigilance +channel-type.airparif.vent.label = Wind +channel-type.airparif.vent.description = Wind event alert level +channel-type.airparif.vent.state.option.0 = No special vigilance +channel-type.airparif.vent.state.option.1 = Be attentive +channel-type.airparif.vent.state.option.2 = Be very vigilant +channel-type.airparif.vent.state.option.3 = Absolute vigilance # discovery result @@ -11,4 +233,5 @@ iconset.description = Icons illustrating air quality measures provided by AirPar # thing status descriptions offline.config-error-unknown-apikey = Parameter 'apikey' must be configured -offline.config-error-invalid-apikey = Parameter 'apikey' is invalid \ No newline at end of file +offline.config-error-invalid-apikey = Parameter 'apikey' is invalid +incorrect-bridge = Wrong bridge type diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml index 86e17c866e2..b1abb517927 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml @@ -10,6 +10,16 @@ Bridge to the AirParif API Portal. In order to receive the data, you must register an account on https://www.airparif.fr/contact and receive your API token. + + + + + + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml new file mode 100644 index 00000000000..89e8600d7e7 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml @@ -0,0 +1,106 @@ + + + + + + + + + Current bulletin validity start + + + + Current bulletin validity start + + + + Current bulletin validity ending + + + + + + + + + + General message for the air quality bulletin + + + + Minimum level of NO2 concentation + + + + Maximum level of NO2 concentation + + + + Minimum level of O3 concentation + + + + Maximum level of O3 concentation + + + + Minimum level of PM 10 concentation + + + + Maximum level of PM 10 concentation + + + + Minimum level of PM 2.5 concentation + + + + Maximum level of PM 2.5 concentation + + + + + + + + + + Current bulletin validity start + + + + Current bulletin validity start + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml index a7876a8ccec..10cba528047 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml @@ -4,196 +4,11 @@ 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"> - - Number - - @text/alertLevelChannelDescription - error - - Alarm - - - - - - - - - - - - - - DateTime - - @text/timestampChannelDescription - time - - - - - String - - - - - - - - - - - - - - - Number - - Rain intensity level - oh:meteofrance:intensity - - - - - - - - - - - - Number - - Wind event alert level - oh:meteofrance:vent - - - - - - - - - - - - Number - - Storm alert level - oh:meteofrance:orage - - - - - - - - - - - - Number - - Flood alert level - oh:meteofrance:inondation - - - - - - - - - - - - Number - - Snow event alert level - oh:meteofrance:neige - - - - - - - - - - - - Number - - High temperature alert level - oh:meteofrance:canicule - - - - - - - - - - - - Number - - Negative temperature alert level - oh:meteofrance:grand-froid - - - - - - - - - - - - Number - - Avalanche alert level - oh:meteofrance:avalanches - - - - - - - - - - - - Number - - Submersion wave alert level - oh:meteofrance:vague-submersion - - - - - - - - - - - - Number - - Flood caused by rainfall alert level - oh:meteofrance:pluie-inondation - - - - - - - - + + DateTime + + time + @@ -203,18 +18,290 @@ + DateTime - + + @text/timestampChannelDescription time - - Image - - Pictogram associated with the alert level. - + + Number + + oh:airparif:hazel + + + + + + + + + + Number + + oh:airparif:birch + + + + + + + + + + + + Number + + oh:airparif:cypress + + + + + + + + + + + + Number + + oh:airparif:alder + + + + + + + + + + + + Number + + oh:airparif:poplar + + + + + + + + + + + Number + + oh:airparif:ash + + + + + + + + + + + Number + + oh:airparif:olive + + + + + + + + + + + + Number + + oh:airparif:urticacea + + + + + + + + + + + + Number + + oh:airparif:wormwood + + + + + + + + + + + + Number + + oh:airparif:rumex + + + + + + + + + + + + Number + + oh:airparif:ragweed + + + + + + + + + + + + Number + + oh:airparif:grasses + + + + + + + + + + + + Number + + oh:airparif:plantain + + + + + + + + + + + + Number + + oh:airparif:chestnut + + + + + + + + + + + + Number + + oh:airparif:oak + + + + + + + + + + + + Number + + oh:airparif:linden + + + + + + + + + + + + Number + + oh:airparif:plane + + + + + + + + + + + + Number + + oh:airparif:hornbeam + + + + + + + + + + + + Number + + oh:airparif:willow + + + + + + + + + + + + Number:Dimensionless + + + + + + Number:Density + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml index 90fdbc3463f..49a01933a7e 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml @@ -5,23 +5,41 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - - - - + + + + + AirParif air quality report for the given location - - - + + + - - - location - - - + department + + + + location + + + + + Name of the department + + + + + + + + + + + true + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/alder.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/alder.svg new file mode 100644 index 00000000000..ea5fc88483e --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/alder.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/ash.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ash.svg new file mode 100644 index 00000000000..9c10179746d --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ash.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/birch.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/birch.svg new file mode 100644 index 00000000000..f8ff505f3b6 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/birch.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/chestnut.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/chestnut.svg new file mode 100644 index 00000000000..8cb9360bf60 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/chestnut.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/cypress.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/cypress.svg new file mode 100644 index 00000000000..0cc76819acd --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/cypress.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/grasses.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/grasses.svg new file mode 100644 index 00000000000..ae6279fd100 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/grasses.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/hazel.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hazel.svg new file mode 100644 index 00000000000..e2dd22c17f3 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hazel.svg @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/hornbeam.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hornbeam.svg new file mode 100644 index 00000000000..3a7d995bfaf --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hornbeam.svg @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/linden.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/linden.svg new file mode 100644 index 00000000000..c10749ecd98 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/linden.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/oak.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/oak.svg new file mode 100644 index 00000000000..343163e1a04 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/oak.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/olive.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/olive.svg new file mode 100644 index 00000000000..719798a0fbf --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/olive.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/plane.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plane.svg new file mode 100644 index 00000000000..ab64767b0f7 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plane.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/plantain.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plantain.svg new file mode 100644 index 00000000000..740cf781f00 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plantain.svg @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/poplar.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/poplar.svg new file mode 100644 index 00000000000..5cd534d8b33 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/poplar.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/ragweed.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ragweed.svg new file mode 100644 index 00000000000..0beffb9f6f5 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ragweed.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/rumex.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/rumex.svg new file mode 100644 index 00000000000..d3607e52a37 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/rumex.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/urticaceae.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/urticaceae.svg new file mode 100644 index 00000000000..07c76f90127 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/urticaceae.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/willow.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/willow.svg new file mode 100644 index 00000000000..bc52029973b --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/willow.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/wormwood.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/wormwood.svg new file mode 100644 index 00000000000..9c02ae1e2e8 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/wormwood.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file