From 12654198344199ed1209f5fe114f3cc0ce92bd24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20L=27hopital?= Date: Fri, 18 Oct 2024 13:41:10 +0200 Subject: [PATCH] Initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaël L'hopital --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.airparif/NOTICE | 13 + .../org.openhab.binding.airparif/README.md | 94 +++ .../org.openhab.binding.airparif/doc/info.txt | 29 + bundles/org.openhab.binding.airparif/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/AirParifBindingConstants.java | 41 + .../airparif/internal/AirParifException.java | 33 + .../internal/AirParifHandlerFactory.java | 65 ++ .../internal/AirParifIconProvider.java | 99 +++ .../airparif/internal/api/AirParifApi.java | 126 +++ .../airparif/internal/api/AirParifDto.java | 127 +++ .../airparif/internal/api/ColorMap.java | 36 + .../internal/api/PollenAlertLevel.java | 43 + .../airparif/internal/api/Pollutant.java | 41 + .../internal/config/BridgeConfiguration.java | 25 + .../config/LocationConfiguration.java | 29 + .../internal/db/DepartmentDbService.java | 77 ++ .../deserialization/AirParifDeserializer.java | 75 ++ .../deserialization/ColorMapDeserializer.java | 42 + .../PollenAlertLevelDeserializer.java | 47 ++ .../PollutantConcentrationDeserializer.java | 52 ++ .../StrictEnumTypeAdapterFactory.java | 64 ++ .../discovery/AirParifDiscoveryService.java | 80 ++ .../handler/AirParifBridgeHandler.java | 176 ++++ .../internal/handler/LocationHandler.java | 78 ++ .../src/main/resources/OH-INF/addon/addon.xml | 12 + .../resources/OH-INF/i18n/airparif.properties | 14 + .../src/main/resources/OH-INF/thing/api.xml | 22 + .../main/resources/OH-INF/thing/channels.xml | 220 +++++ .../resources/OH-INF/thing/thing-types.xml | 27 + .../src/main/resources/db/departments.json | 770 ++++++++++++++++++ .../src/main/resources/icon/average.svg | 7 + .../src/main/resources/icon/bad.svg | 9 + .../src/main/resources/icon/degrated.svg | 7 + .../src/main/resources/icon/extremely-bad.svg | 6 + .../src/main/resources/icon/good.svg | 8 + .../src/main/resources/icon/pollen.svg | 4 + 39 files changed, 2630 insertions(+) create mode 100755 bundles/org.openhab.binding.airparif/NOTICE create mode 100755 bundles/org.openhab.binding.airparif/README.md create mode 100644 bundles/org.openhab.binding.airparif/doc/info.txt create mode 100755 bundles/org.openhab.binding.airparif/pom.xml create mode 100755 bundles/org.openhab.binding.airparif/src/main/feature/feature.xml create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifException.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifHandlerFactory.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifIconProvider.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifApi.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/ColorMap.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/PollenAlertLevel.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/Pollutant.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/BridgeConfiguration.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/LocationConfiguration.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/AirParifDeserializer.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/ColorMapDeserializer.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java create mode 100644 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollutantConcentrationDeserializer.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/StrictEnumTypeAdapterFactory.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/discovery/AirParifDiscoveryService.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java create mode 100755 bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/addon/addon.xml create mode 100755 bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties create mode 100755 bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml create mode 100755 bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml create mode 100755 bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.airparif/src/main/resources/db/departments.json create mode 100644 bundles/org.openhab.binding.airparif/src/main/resources/icon/average.svg create mode 100644 bundles/org.openhab.binding.airparif/src/main/resources/icon/bad.svg create mode 100644 bundles/org.openhab.binding.airparif/src/main/resources/icon/degrated.svg create mode 100644 bundles/org.openhab.binding.airparif/src/main/resources/icon/extremely-bad.svg create mode 100644 bundles/org.openhab.binding.airparif/src/main/resources/icon/good.svg create mode 100644 bundles/org.openhab.binding.airparif/src/main/resources/icon/pollen.svg diff --git a/CODEOWNERS b/CODEOWNERS index 0526f553154..d2c5193f1ac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -15,6 +15,7 @@ /bundles/org.openhab.binding.adorne/ @theiding /bundles/org.openhab.binding.ahawastecollection/ @soenkekueper /bundles/org.openhab.binding.airgradient/ @austvik +/bundles/org.openhab.binding.airparif/ @clinique /bundles/org.openhab.binding.airq/ @aurelio1 @fwolter /bundles/org.openhab.binding.airquality/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.airvisualnode/ @3cky diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 170f8ce163d..754336edb1e 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -66,6 +66,11 @@ org.openhab.binding.airgradient ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.airparif + ${project.version} + org.openhab.addons.bundles org.openhab.binding.airq diff --git a/bundles/org.openhab.binding.airparif/NOTICE b/bundles/org.openhab.binding.airparif/NOTICE new file mode 100755 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.airparif/README.md b/bundles/org.openhab.binding.airparif/README.md new file mode 100755 index 00000000000..d7a6687bb29 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/README.md @@ -0,0 +1,94 @@ +# AirParif Binding + +_Give some details about what this binding is meant for - a protocol, system, specific device._ + +_If possible, provide some resources like pictures (only PNG is supported currently), a video, etc. to give an impression of what can be done with this binding._ +_You can place such resources into a `doc` folder next to this README.md._ + +_Put each sentence in a separate line to improve readability of diffs._ + +## Supported Things + +_Please describe the different supported things / devices including their ThingTypeUID within this section._ +_Which different types are supported, which models were tested etc.?_ +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +- `bridge`: Short description of the Bridge, if any +- `sample`: Short description of the Thing with the ThingTypeUID `sample` + +## Discovery + +_Describe the available auto-discovery features here._ +_Mention for what it works and what needs to be kept in mind when using it._ + +## Binding Configuration + +_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```.cfg``` inside it._ +_In this section, you should link to this file and provide some information about the options._ +_The file could e.g. look like:_ + +``` +# Configuration for the AirParif Binding +# +# Default secret key for the pairing of the AirParif Thing. +# It has to be between 10-40 (alphanumeric) characters. +# This may be changed by the user for security reasons. +secret=openHABSecret +``` + +_Note that it is planned to generate some part of this based on the information that is available within ```src/main/resources/OH-INF/binding``` of your binding._ + +_If your binding does not offer any generic configurations, you can remove this section completely._ + +## Thing Configuration + +_Describe what is needed to manually configure a thing, either through the UI or via a thing-file._ +_This should be mainly about its mandatory and optional configuration parameters._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +### `sample` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|---------------------------------------|---------|----------|----------| +| hostname | text | Hostname or IP address of the device | N/A | yes | no | +| password | text | Password to access the device | N/A | yes | no | +| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes | + +## Channels + +_Here you should provide information about available channel types, what their meaning is and how they can be used._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +| Channel | Type | Read/Write | Description | +|---------|--------|------------|-----------------------------| +| control | Switch | RW | This is the control channel | + +## Full Example + +_Provide a full usage example based on textual configuration files._ +_*.things, *.items examples are mandatory as textual configuration is well used by many users._ +_*.sitemap examples are optional._ + +### Thing Configuration + +```java +Example thing configuration goes here. +``` +### Item Configuration + +```java +Example item configuration goes here. +``` + +### Sitemap Configuration + +```perl +Optional Sitemap configuration goes here. +Remove this section, if not needed. +``` + +## Any custom content here! + +_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_ diff --git a/bundles/org.openhab.binding.airparif/doc/info.txt b/bundles/org.openhab.binding.airparif/doc/info.txt new file mode 100644 index 00000000000..53c913750cc --- /dev/null +++ b/bundles/org.openhab.binding.airparif/doc/info.txt @@ -0,0 +1,29 @@ + +Miguel Narvaez Miguel.Narvaez@airparif.fr via improvmx-mails.com +30 sept. 2024 10:06 (il y a 11 jours) +À gael@lhopital.org, api + +Bonjour, + +Veuillez trouver ci-dessous la clé API vous permettant d’accéder à nos API Indices de la qualité de l'air, Épisodes, Cartographie, Pollens : + +5a923300-e93d-2a0f-4321-96f115f19100 + +Cette clé sera active à partir de demain. + +Nous ouvrons ces API dans une démarche d’amélioration continue, n’hésitez pas à revenir vers nous pour des retours d’expérience et des propositions d’améliorations. + +La documentation des APIs se trouve à l’adresse suivante : +https://api.airparif.fr/docs + +Celle concernant les services Web de cartographie se trouve ici : +https://www.airparif.fr/doc_api_carto/ + +Les informations qui vous concernent sont destinées uniquement à Airparif. Votre adresse e-mail sera utilisée uniquement pour vous informer à propos des API. Vous disposez d'un droit d'accès, de modification, de rectification et de suppression de ces données (art. 34 de la loi "Informatique et Libertés"). + +Pour toute question concernant vos données personnelles ou nos API, nous vous invitons à nous contacter via notre formulaire en ligne : +https://www.airparif.asso.fr/contact + +Bonne journée ! + +Airparif \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/pom.xml b/bundles/org.openhab.binding.airparif/pom.xml new file mode 100755 index 00000000000..c38d9fba93e --- /dev/null +++ b/bundles/org.openhab.binding.airparif/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.airparif + + openHAB Add-ons :: Bundles :: AirParif Binding + + diff --git a/bundles/org.openhab.binding.airparif/src/main/feature/feature.xml b/bundles/org.openhab.binding.airparif/src/main/feature/feature.xml new file mode 100755 index 00000000000..d4a2170fcfa --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.airparif/${project.version} + + 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 new file mode 100755 index 00000000000..6ed929d4c87 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link AirParifBindingConstants} class defines common constants, which are used across the whole binding. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class AirParifBindingConstants { + public static final String BINDING_ID = "airparif"; + public static final String LOCAL = "local"; + + // List of Bridge Type UIDs + public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "api"); + + // List of Things Type UIDs + public static final ThingTypeUID LOCATION_THING_TYPE = new ThingTypeUID(BINDING_ID, "location"); + + // List of all Channel ids + public static final String CHANNEL_1 = "channel1"; + + 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/AirParifException.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifException.java new file mode 100755 index 00000000000..3d7f00492de --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifException.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An exception that occurred while communicating with Air Parif API server or related processes. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class AirParifException extends Exception { + private static final long serialVersionUID = 4234683995736417341L; + + public AirParifException(String format, Object... args) { + super(format.formatted(args)); + } + + public AirParifException(Exception e, String format, Object... args) { + super(format.formatted(args), e); + } +} 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 new file mode 100755 index 00000000000..ea9f49da607 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifHandlerFactory.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal; + +import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer; +import org.openhab.binding.airparif.internal.handler.AirParifBridgeHandler; +import org.openhab.binding.airparif.internal.handler.LocationHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link AirParifHandlerFactory} is responsible for creating things and thing handlers. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.airparif", service = ThingHandlerFactory.class) +public class AirParifHandlerFactory extends BaseThingHandlerFactory { + private final AirParifDeserializer deserializer; + private final HttpClient httpClient; + + @Activate + public AirParifHandlerFactory(final @Reference HttpClientFactory httpClientFactory, + final @Reference AirParifDeserializer deserializer) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.deserializer = deserializer; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + return APIBRIDGE_THING_TYPE.equals(thingTypeUID) + ? new AirParifBridgeHandler((Bridge) thing, httpClient, deserializer) + : 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 new file mode 100755 index 00000000000..7a9007f2625 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifIconProvider.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal; + +import static org.openhab.binding.airparif.internal.AirParifBindingConstants.BINDING_ID; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.ui.icon.IconProvider; +import org.openhab.core.ui.icon.IconSet; +import org.openhab.core.ui.icon.IconSet.Format; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AirParifIconProvider} is the class providing binding related icons. + * + * @author Gaël L'hopital - Initial contribution + */ +@Component(service = { IconProvider.class, AirParifIconProvider.class }) +@NonNullByDefault +public class AirParifIconProvider implements IconProvider { + 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 final Logger logger = LoggerFactory.getLogger(AirParifIconProvider.class); + private final TranslationProvider i18nProvider; + private final Bundle bundle; + + @Activate + public AirParifIconProvider(final BundleContext context, final @Reference TranslationProvider i18nProvider) { + this.i18nProvider = i18nProvider; + this.bundle = context.getBundle(); + } + + @Override + public Set getIconSets() { + return getIconSets(null); + } + + @Override + public Set getIconSets(@Nullable Locale locale) { + String label = getText("label", DEFAULT_LABEL, locale); + String description = getText("decription", DEFAULT_DESCRIPTION, locale); + + return Set.of(new IconSet(BINDING_ID, label, description, Set.of(Format.SVG))); + } + + private String getText(String entry, String defaultValue, @Nullable Locale locale) { + String text = locale == null ? null : i18nProvider.getText(bundle, "iconset." + entry, defaultValue, locale); + return text == null ? defaultValue : text; + } + + @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; + } + + @Override + public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) { + URL iconResource = bundle.getEntry("icon/%s.svg".formatted(category)); + + String result; + try (InputStream stream = iconResource.openStream()) { + result = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + logger.warn("Unable to load ressource '{}': {}", iconResource.getPath(), e.getMessage()); + result = ""; + } + + return result.isEmpty() ? null : new ByteArrayInputStream(result.getBytes()); + } +} diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifApi.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifApi.java new file mode 100644 index 00000000000..95f387064b3 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifApi.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.api; + +import java.net.URI; +import java.util.EnumSet; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link AirParifApi} class defines paths used to interact with server api + * + * @author Gaël L'hopital - Initial contribution + * + */ +@NonNullByDefault +public class AirParifApi { + private static final UriBuilder AIRPARIF_BUILDER = UriBuilder.fromPath("/").scheme("https").host("api.airparif.fr"); + public static final URI VERSION_URI = AIRPARIF_BUILDER.clone().path("version").build(); + public static final URI KEY_INFO_URI = AIRPARIF_BUILDER.clone().path("key-info").build(); + public static final URI HORAIR_URI = AIRPARIF_BUILDER.clone().path("horair").path("itineraire").build(); + public static final URI EPISODES_URI = AIRPARIF_BUILDER.clone().path("episodes").path("en-cours-et-prevus").build(); + + private static final UriBuilder INDICES_BUILDER = AIRPARIF_BUILDER.clone().path("indices").path("prevision"); + public static final URI PREV_COLORS_URI = INDICES_BUILDER.clone().path("couleurs").build(); + public static final URI PREV_BULLETIN_URI = INDICES_BUILDER.clone().path("bulletin").build(); + + private static final UriBuilder POLLENS_BUILDER = AIRPARIF_BUILDER.clone().path("pollens"); + public static final URI POLLENS_URI = POLLENS_BUILDER.clone().path("bulletin").build(); + + // Poor interest, only returns highest risk level for the dept. + // public static final UriBuilder POLLENS_DEPT_BUILDER = POLLENS_BUILDER.clone().path("departement"); + + public enum Scope { + @SerializedName("Cartes et résultats Hor'Air") + MAPS, + @SerializedName("Pollens") + POLLENS, + @SerializedName("Épisodes") + EVENTS, + @SerializedName("Indices") + INDEXES, + UNKNOWN; + } + + public enum Appreciation { + GOOD("Bon"), + AVERAGE("Moyen"), + DEGRATED("Dégradé"), + BAD("Mauvais"), + REALLY_BAD("Très Mauvais"), + EXTREMELY_BAD("Extrêmement Mauvais"), + UNKNOWN(""); + + public final String apiName; + + Appreciation(String apiName) { + this.apiName = apiName; + } + + public static final EnumSet AS_SET = EnumSet.allOf(Appreciation.class); + } + + public enum Pollen { + @SerializedName("cypres") + CYPRESS("cypres"), + @SerializedName("noisetier") + HAZEL("noisetier"), + @SerializedName("aulne") + ALDER("aulne"), + @SerializedName("peuplier") + POPLAR("peuplier"), + @SerializedName("saule") + WILLOW("saule"), + @SerializedName("frene") + ASH("frene"), + @SerializedName("charme") + HORNBEAM("charme"), + @SerializedName("bouleau") + BIRCH("bouleau"), + @SerializedName("platane") + PLANE("platane"), + @SerializedName("chene") + OAK("chene"), + @SerializedName("olivier") + OLIVE("olivier"), + @SerializedName("tilleul") + LINDEN("tilleul"), + @SerializedName("chataignier") + CHESTNUT("chataignier"), + @SerializedName("rumex") + RUMEX("rumex"), + @SerializedName("graminees") + GRASSES("graminees"), + @SerializedName("plantain") + PLANTAIN("plantain"), + @SerializedName("urticacees") + URTICACEAE("urticacees"), + @SerializedName("armoises") + WORMWOOD("armoises"), + @SerializedName("ambroisies") + RAGWEED("ambroisies"); + + public final String apiName; + + Pollen(String apiName) { + this.apiName = apiName; + } + + public static final EnumSet AS_SET = EnumSet.allOf(Pollen.class); + } +} 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 new file mode 100644 index 00000000000..bb2238f8692 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.api; + +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.AirParifApi.Scope; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link AirParifDto} class defines DTO used to interact with server api + * + * @author Gaël L'hopital - Initial contribution + * + */ +@NonNullByDefault +public class AirParifDto { + public record Version(// + String version) { + } + + public record KeyInfo(// + ZonedDateTime expiration, // + @SerializedName("droits") Set scopes) { + } + + private record Message(// + String fr, // + @Nullable String en) { + } + + public record PollutantConcentration(// + Pollutant pollutant, // + int min, // + int max) { + } + + public record PollutantEpisode(// + @SerializedName("nom") Pollutant pollutant, // + @SerializedName("niveau") String level) { + } + + public record DailyBulletin(// + @SerializedName("date") LocalDate previsionDate, // + @SerializedName("date_previ") LocalDate productionDate, // + @SerializedName("disponible") boolean available, // + Message bulletin, // + Set concentrations) { + public String dayDescription() { + return bulletin.fr; + } + } + + public record DailyEpisode(// + @SerializedName("actif") boolean active, // + @SerializedName("polluants") Set pollutants) { + } + + public record Bulletin( // + @SerializedName("jour") DailyBulletin today, // + @SerializedName("demain") DailyBulletin tomorrow) { + } + + public record Episode( // + @SerializedName("actif") boolean active, Message message, @SerializedName("jour") DailyEpisode today, // + @SerializedName("demain") DailyEpisode tomorrow) { + } + + public record Pollens(// + Pollen[] taxons, // + Map valeurs, // + String commentaire, // + String periode) { + + private static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yy"); + private static Pattern PATTERN = Pattern.compile("\\d{2}.\\d{2}.\\d{2}"); + + 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); + } + if (matcher.find()) { + extractedDate = matcher.group(); + return LocalDate.parse(extractedDate, FORMATTER); + } + } + return null; + } + + public @Nullable LocalDate beginValidity() { + return getValidity(periode, true); + } + + public @Nullable LocalDate endValidity() { + return getValidity(periode, false); + } + + } + + public record PollensResponse(ArrayList data) { + } + +} 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 new file mode 100644 index 00000000000..59aa08af91d --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/ColorMap.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.api; + +import java.util.HashMap; +import java.util.Objects; + +import org.openhab.binding.airparif.internal.api.AirParifApi.Appreciation; + +/** + * Class association between air quality appreciation and its color + * + * @author Gaël L'hopital - Initial contribution + */ +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)); + } + + public String put(String key, String value) { + return super.put(fromApiName(key), 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 new file mode 100644 index 00000000000..74b2517cded --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/PollenAlertLevel.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.api; + +import java.util.EnumSet; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public enum PollenAlertLevel { + @SerializedName("0") + NONE(0), + @SerializedName("1") + LOW(1), + @SerializedName("2") + AVERAGE(2), + @SerializedName("3") + HIGH(3), + UNKNOWN(-1); + + public static final EnumSet AS_SET = EnumSet.allOf(PollenAlertLevel.class); + + public final int riskLevel; + + PollenAlertLevel(int riskLevel) { + this.riskLevel = riskLevel; + } +} 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 new file mode 100644 index 00000000000..21a1163a90b --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/Pollutant.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.api; + +import java.util.EnumSet; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Pollutant} enum lists all pollutants tracked by AirParif + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public enum Pollutant { + PM25, + PM10, + NO2, + O3, + UNKNOWN; + + public static final EnumSet AS_SET = EnumSet.allOf(Pollutant.class); + + public static Pollutant safeValueOf(String searched) { + try { + return Pollutant.valueOf(searched); + } catch (IllegalArgumentException e) { + return Pollutant.UNKNOWN; + } + } +} diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/BridgeConfiguration.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/BridgeConfiguration.java new file mode 100755 index 00000000000..710581478df --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/BridgeConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link BridgeConfiguration} is the class used to match the bridge configuration. + * + * @author Gaël L"hopital - Initial contribution + */ +@NonNullByDefault +public class BridgeConfiguration { + public String apikey = ""; +} 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 new file mode 100755 index 00000000000..0053cf0f9f5 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/LocationConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LocationConfiguration} is the class used to match the + * thing configuration. + * + * @author Gaël L"hopital - Initial contribution + */ +@NonNullByDefault +public class LocationConfiguration { + public static final String LOCATION = "location"; + + public int refresh = 10; + public String location = ""; +} 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 new file mode 100644 index 00000000000..b9278becd4f --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.db; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +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; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link DepartmentDbService} makes available a list of known French Metropolitan departments. + * + * @author Gaël L'hopital - Initial Contribution + */ + +@Component(service = DepartmentDbService.class) +@NonNullByDefault +public class DepartmentDbService { + private final Logger logger = LoggerFactory.getLogger(DepartmentDbService.class); + private final List departments = new ArrayList<>(); + + public record Department(String id, String name, double northestLat, double southestLat, double eastestLon, + double westestLon) { + } + + @Activate + public DepartmentDbService() { + try (InputStream is = Thread.currentThread().getContextClassLoader() + .getResourceAsStream("/db/departments.json"); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + departments.addAll(Arrays.asList(gson.fromJson(reader, Department[].class))); + logger.debug("Successfully loaded {} departments", departments.size()); + } catch (IOException | JsonSyntaxException | JsonIOException e) { + logger.warn("Unable to load departments list: {}", e.getMessage()); + } + } + + public List getBounding(PointType location) { + double latitude = location.getLatitude().doubleValue(); + double longitude = location.getLongitude().doubleValue(); + return departments.stream().filter(dep -> dep.northestLat >= latitude && dep.southestLat <= latitude + && dep.westestLon <= longitude && dep.eastestLon >= longitude).toList(); + } + + public @Nullable Department getDept(String deptId) { + return departments.stream().filter(dep -> dep.id.equalsIgnoreCase(deptId)).findFirst().orElse(null); + } +} 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 new file mode 100755 index 00000000000..a96b359ea00 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/AirParifDeserializer.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.deserialization; + +import java.time.LocalDate; +import java.time.ZonedDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.airparif.internal.AirParifException; +import org.openhab.binding.airparif.internal.api.AirParifDto.PollutantConcentration; +import org.openhab.binding.airparif.internal.api.ColorMap; +import org.openhab.binding.airparif.internal.api.PollenAlertLevel; +import org.openhab.core.i18n.TimeZoneProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link AirParifDeserializer} is responsible to instantiate suitable Gson (de)serializer + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +@Component(service = AirParifDeserializer.class) +public class AirParifDeserializer { + private final Gson gson; + + @Activate + public AirParifDeserializer(final @Reference TimeZoneProvider timeZoneProvider) { + gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .registerTypeAdapter(PollenAlertLevel.class, new PollenAlertLevelDeserializer()) + .registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory()) + .registerTypeAdapter(ColorMap.class, new ColorMapDeserializer()) + .registerTypeAdapter(PollutantConcentration.class, new PollutantConcentrationDeserializer()) + .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(); + } + + public T deserialize(Class clazz, String json) throws AirParifException { + try { + @Nullable + T result = gson.fromJson(json, clazz); + if (result != null) { + return result; + } + throw new AirParifException("Deserialization of '%s' resulted in null value", json); + } catch (JsonSyntaxException e) { + throw new AirParifException(e, "Unexpected error deserializing '%s'", json); + } + } + +} diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/ColorMapDeserializer.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/ColorMapDeserializer.java new file mode 100644 index 00000000000..c0804753b37 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/ColorMapDeserializer.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.deserialization; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.airparif.internal.api.ColorMap; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; + +/** + * Specialized deserializer for ColorMap class + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +class ColorMapDeserializer implements JsonDeserializer { + + @Override + public @Nullable ColorMap deserialize(JsonElement json, Type clazz, JsonDeserializationContext context) { + ColorMap result = new ColorMap(); + Set> entrySet = json.getAsJsonObject().entrySet(); + entrySet.forEach(entry -> result.put(entry.getKey(), entry.getValue().getAsString())); + return result; + } +} 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 new file mode 100644 index 00000000000..112796875e2 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.deserialization; + +import java.lang.reflect.Type; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.airparif.internal.api.PollenAlertLevel; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; + +/** + * Specialized deserializer for ColorMap class + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +class PollenAlertLevelDeserializer implements JsonDeserializer { + + @Override + public @Nullable PollenAlertLevel deserialize(JsonElement json, Type clazz, JsonDeserializationContext context) { + int level; + try { + level = json.getAsInt(); + } 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/deserialization/PollutantConcentrationDeserializer.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollutantConcentrationDeserializer.java new file mode 100644 index 00000000000..d66045b6d77 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollutantConcentrationDeserializer.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.deserialization; + +import java.lang.reflect.Type; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.airparif.internal.api.AirParifDto.PollutantConcentration; +import org.openhab.binding.airparif.internal.api.Pollutant; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; + +/** + * Specialized deserializer for ColorMap class + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +class PollutantConcentrationDeserializer implements JsonDeserializer { + + @Override + public @Nullable PollutantConcentration deserialize(JsonElement json, Type clazz, + JsonDeserializationContext context) { + PollutantConcentration result = null; + JsonArray array = json.getAsJsonArray(); + + if (array.size() == 3) { + Pollutant pollutant = Pollutant.safeValueOf(array.get(0).getAsString()); + try { + result = new PollutantConcentration(pollutant, array.get(1).getAsInt(), array.get(2).getAsInt()); + } catch (JsonSyntaxException ignore) { + // result will remain null + } + } + return result; + } +} diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/StrictEnumTypeAdapterFactory.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/StrictEnumTypeAdapterFactory.java new file mode 100755 index 00000000000..a68f243895e --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/StrictEnumTypeAdapterFactory.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.deserialization; + +import java.io.IOException; +import java.io.StringReader; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Enforces a fallback to UNKNOWN when deserializing enum types, marked as @NonNull whereas they were valued + * to null if the appropriate value is absent. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +class StrictEnumTypeAdapterFactory implements TypeAdapterFactory { + + @Override + public @Nullable TypeAdapter create(@NonNullByDefault({}) Gson gson, + @NonNullByDefault({}) TypeToken type) { + return type.getRawType().isEnum() ? newStrictEnumAdapter(gson.getDelegateAdapter(this, type)) : null; + } + + private TypeAdapter newStrictEnumAdapter(@NonNullByDefault({}) TypeAdapter delegateAdapter) { + return new TypeAdapter<>() { + @Override + public void write(JsonWriter out, @Nullable T value) throws IOException { + delegateAdapter.write(out, value); + } + + @Override + public @Nullable T read(JsonReader in) throws IOException { + JsonReader delegateReader = new JsonReader( + new StringReader('"' + in.nextString().replace(",", "") + '"')); + @Nullable + T value = delegateAdapter.read(delegateReader); + delegateReader.close(); + if (value == null) { + value = delegateAdapter.read(new JsonReader(new StringReader("\"UNKNOWN\""))); + } + return value; + } + }; + } +} 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 new file mode 100755 index 00000000000..6df88a8a2d1 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/discovery/AirParifDiscoveryService.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.discovery; + +import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*; +import static org.openhab.binding.airparif.internal.config.LocationConfiguration.LOCATION; + +import org.eclipse.jdt.annotation.NonNullByDefault; +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; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AirParifDiscoveryService} creates things based on the configured location. + * + * @author Gaël L'hopital - Initial Contribution + */ +@Component(scope = ServiceScope.PROTOTYPE, service = AirParifDiscoveryService.class) +@NonNullByDefault +public class AirParifDiscoveryService extends AbstractThingHandlerDiscoveryService { + private static final int DISCOVER_TIMEOUT_SECONDS = 2; + + private final Logger logger = LoggerFactory.getLogger(AirParifDiscoveryService.class); + + private @NonNullByDefault({}) LocationProvider locationProvider; + + public AirParifDiscoveryService() { + super(AirParifBridgeHandler.class, SUPPORTED_THING_TYPES_UIDS, DISCOVER_TIMEOUT_SECONDS); + } + + @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) { + this.locationProvider = locationProvider; + } + + @Override + protected 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()); + } else { + logger.debug("LocationProvider.getLocation() is not set, no discovery results can be provided"); + } + } +} 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 new file mode 100755 index 00000000000..bb2883ee288 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.handler; + +import static org.openhab.binding.airparif.internal.api.AirParifApi.*; + +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +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.api.AirParifDto.Bulletin; +import org.openhab.binding.airparif.internal.api.AirParifDto.Episode; +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.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.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link AirParifBridgeHandler} is the handler for OpenUV API and connects it + * to the webservice. + * + * @author Gaël L'hopital - Initial contribution + * + */ +@NonNullByDefault +public class AirParifBridgeHandler extends BaseBridgeHandler { + private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30); + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + private final Logger logger = LoggerFactory.getLogger(AirParifBridgeHandler.class); + private final AirParifDeserializer deserializer; + private final HttpClient httpClient; + + private BridgeConfiguration config = new BridgeConfiguration(); + + public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer) { + super(bridge); + this.deserializer = deserializer; + this.httpClient = httpClient; + } + + @Override + public void initialize() { + logger.debug("Initializing AirParif bridge handler."); + config = getConfigAs(BridgeConfiguration.class); + if (config.apikey.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.config-error-unknown-apikey"); + return; + } + initiateConnexion(); + } + + public synchronized String executeUri(URI uri) throws AirParifException { + logger.debug("executeUrl: {} ", uri); + + Request request = httpClient.newRequest(uri).method(HttpMethod.GET) + .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .header(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON).header("X-Api-Key", config.apikey); + + try { + ContentResponse response = request.send(); + + Code statusCode = HttpStatus.getCode(response.getStatus()); + + if (statusCode == Code.OK) { + String content = new String(response.getContent(), DEFAULT_CHARSET); + logger.trace("executeUrl: {} returned {}", uri, content); + return content; + } else if (statusCode == Code.FORBIDDEN) { + throw new AirParifException("@text/offline.config-error-invalid-apikey"); + } + + 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()); + } catch (InterruptedException e) { + throw new AirParifException(e, "Execution interrupted: %s", e.getMessage()); + } + } + + public synchronized T executeUri(URI uri, Class clazz) throws AirParifException { + String content = executeUri(uri); + return deserializer.deserialize(clazz, content); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("The AirParif bridge does not handles commands"); + } + + private void initiateConnexion() { + Version version; + KeyInfo keyInfo; + + try { // This does validate communication with the server + version = executeUri(VERSION_URI, Version.class); + } catch (AirParifException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + return; + } + + try { // This validates the api key value + keyInfo = executeUri(KEY_INFO_URI, KeyInfo.class); + } catch (AirParifException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + return; + } + + 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(","))); + updateStatus(ThingStatus.ONLINE); + + try { + ColorMap map = executeUri(PREV_COLORS_URI, ColorMap.class); + logger.info("The color map is {}", map.toString()); + + Bulletin bulletin = executeUri(PREV_BULLETIN_URI, Bulletin.class); + logger.info("The bulletin is {}", bulletin.today().dayDescription()); + + Episode episode = executeUri(EPISODES_URI, Episode.class); + logger.info("The bulletin 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(); + + String response = executeUri(POLLENS_DEPT_BUILDER.path("78").build()); + logger.info("The pollens 78 {}", response); + } catch (AirParifException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + return; + } + } + +} 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 new file mode 100755 index 00000000000..11450b1ecb2 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.handler; + +import static org.openhab.binding.airparif.internal.AirParifBindingConstants.CHANNEL_1; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.airparif.internal.config.LocationConfiguration; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +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; + +/** + * The {@link LocationHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class LocationHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(LocationHandler.class); + + private @Nullable LocationConfiguration config; + + public LocationHandler(Thing thing) { + 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); + } else { + updateStatus(ThingStatus.OFFLINE); + } + }); + } +} 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 new file mode 100755 index 00000000000..74286c6bb4c --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,12 @@ + + + + binding + AirParif Binding + Air Quality data and forecasts provided by AirParif. + 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 new file mode 100755 index 00000000000..98191f8a6c2 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties @@ -0,0 +1,14 @@ + +# discovery result + +discovery.airparif.location.local.label = Air Quality Report + +# iconprovider + +iconset.label = AirParif Icons +iconset.description = Icons illustrating air quality measures provided by AirParif + +# 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 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 new file mode 100755 index 00000000000..86e17c866e2 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml @@ -0,0 +1,22 @@ + + + + + + + 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. + + + + + Token used to access the service + password + + + + + 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 new file mode 100755 index 00000000000..a7876a8ccec --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml @@ -0,0 +1,220 @@ + + + + + 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 + + + + + + + + + + + + String + + text + + + + + DateTime + + time + + + + + Image + + Pictogram associated with the alert level. + + + + 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 new file mode 100755 index 00000000000..90fdbc3463f --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,27 @@ + + + + + + + + + + AirParif air quality report for the given location + + + + + + + + location + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/db/departments.json b/bundles/org.openhab.binding.airparif/src/main/resources/db/departments.json new file mode 100644 index 00000000000..afa41c50c61 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/db/departments.json @@ -0,0 +1,770 @@ +[ + { + "id": "01", + "name": "Ain", + "northest_lat": 46.517199374492, + "southest_lat": 45.611235124481, + "eastest_lon": 6.1697363568789, + "westest_lon": 4.729097034294 + }, + { + "id": "02", + "name": "Aisne", + "northest_lat": 50.069271974234, + "southest_lat": 48.837795469902, + "eastest_lon": 4.2540638814402, + "westest_lon": 2.9624508927558 + }, + { + "id": "03", + "name": "Allier", + "northest_lat": 46.803872076205, + "southest_lat": 45.930727869968, + "eastest_lon": 4.0055701432229, + "westest_lon": 2.2804029533754 + }, + { + "id": "04", + "name": "Alpes-de-Haute-Provence", + "northest_lat": 44.659501345682, + "southest_lat": 43.668282105491, + "eastest_lon": 6.9668199032047, + "westest_lon": 5.4980104773442 + }, + { + "id": "05", + "name": "Hautes-Alpes", + "northest_lat": 45.12684420383, + "southest_lat": 44.186478874057, + "eastest_lon": 7.0771048243018, + "westest_lon": 5.4185330627929 + }, + { + "id": "06", + "name": "Alpes-Maritimes", + "northest_lat": 44.361051257141, + "southest_lat": 43.480065494401, + "eastest_lon": 7.7169378581589, + "westest_lon": 6.6363906079569 + }, + { + "id": "07", + "name": "Ardèche", + "northest_lat": 45.365674921417, + "southest_lat": 44.264783733394, + "eastest_lon": 4.8865943991285, + "westest_lon": 3.8615128126047 + }, + { + "id": "08", + "name": "Ardennes", + "northest_lat": 50.168317417174, + "southest_lat": 49.228510768771, + "eastest_lon": 5.3935393812988, + "westest_lon": 4.0252899216328 + }, + { + "id": "09", + "name": "Ariège", + "northest_lat": 43.315601482419, + "southest_lat": 42.572397819345, + "eastest_lon": 2.1758766074869, + "westest_lon": 0.82612266137771 + }, + { + "id": "10", + "name": "Aube", + "northest_lat": 48.716672523486, + "southest_lat": 47.924112631788, + "eastest_lon": 4.863174195777, + "westest_lon": 3.3883584814447 + }, + { + "id": "11", + "name": "Aude", + "northest_lat": 43.459927734352, + "southest_lat": 42.64890098195, + "eastest_lon": 3.2405623482295, + "westest_lon": 1.6884233932357 + }, + { + "id": "12", + "name": "Aveyron", + "northest_lat": 44.941219492647, + "southest_lat": 43.692056102371, + "eastest_lon": 3.4507554815828, + "westest_lon": 1.8396044963184 + }, + { + "id": "13", + "name": "Bouches-du-Rhône", + "northest_lat": 43.92406219253, + "southest_lat": 43.162545513212, + "eastest_lon": 5.8132476569219, + "westest_lon": 4.2302808850321 + }, + { + "id": "14", + "name": "Calvados", + "northest_lat": 49.429859840723, + "southest_lat": 48.752223582274, + "eastest_lon": 0.44627431224134, + "westest_lon": -1.1595014604014 + }, + { + "id": "15", + "name": "Cantal", + "northest_lat": 45.480702895801, + "southest_lat": 44.61552895784, + "eastest_lon": 3.3699091459722, + "westest_lon": 2.0629079799591 + }, + { + "id": "16", + "name": "Charente", + "northest_lat": 46.139591492141, + "southest_lat": 45.191628193392, + "eastest_lon": 0.9456207917489, + "westest_lon": -0.46177341555077 + }, + { + "id": "17", + "name": "Charente-Maritime", + "northest_lat": 46.371051399922, + "southest_lat": 45.088810634907, + "eastest_lon": 0.005823224821197, + "westest_lon": -1.5614800452621 + }, + { + "id": "18", + "name": "Cher", + "northest_lat": 47.628965936616, + "southest_lat": 46.420403547753, + "eastest_lon": 3.0793324170792, + "westest_lon": 1.7745852665449 + }, + { + "id": "19", + "name": "Corrèze", + "northest_lat": 45.763895555756, + "southest_lat": 44.923721627249, + "eastest_lon": 2.5283596411119, + "westest_lon": 1.2271245972559 + }, + { + "id": "21", + "name": "Côte-d'Or", + "northest_lat": 48.030241950581, + "southest_lat": 46.900857518168, + "eastest_lon": 5.5185372800929, + "westest_lon": 4.0660574486622 + }, + { + "id": "22", + "name": "Côtes-d'Armor", + "northest_lat": 48.867411825697, + "southest_lat": 48.035478415172, + "eastest_lon": -1.9089921410274, + "westest_lon": -3.663669588163 + }, + { + "id": "23", + "name": "Creuse", + "northest_lat": 46.45481310551, + "southest_lat": 45.664008285608, + "eastest_lon": 2.6107853057918, + "westest_lon": 1.3748978470741 + }, + { + "id": "24", + "name": "Dordogne", + "northest_lat": 45.714569962764, + "southest_lat": 44.57129825478, + "eastest_lon": 1.4482602497483, + "westest_lon": -0.041998525054412 + }, + { + "id": "25", + "name": "Doubs", + "northest_lat": 47.579897594928, + "southest_lat": 46.553996454277, + "eastest_lon": 7.0622006908671, + "westest_lon": 5.6987272452696 + }, + { + "id": "26", + "name": "Drôme", + "northest_lat": 45.344042230781, + "southest_lat": 44.115716677493, + "eastest_lon": 5.8294720463131, + "westest_lon": 4.6477668446587 + }, + { + "id": "27", + "name": "Eure", + "northest_lat": 49.485111873749, + "southest_lat": 48.666521479531, + "eastest_lon": 1.8026740663848, + "westest_lon": 0.29722451460974 + }, + { + "id": "28", + "name": "Eure-et-Loir", + "northest_lat": 48.941051842112, + "southest_lat": 47.953852019595, + "eastest_lon": 1.9940901445311, + "westest_lon": 0.76023175104941 + }, + { + "id": "29", + "name": "Finistère", + "northest_lat": 48.75230874743, + "southest_lat": 47.762638930067, + "eastest_lon": -3.3880788564101, + "westest_lon": -5.138001239929 + }, + { + "id": "30", + "name": "Gard", + "northest_lat": 44.459798467391, + "southest_lat": 43.460183661653, + "eastest_lon": 4.8455501032842, + "westest_lon": 3.2628340569911 + }, + { + "id": "31", + "name": "Haute-Garonne", + "northest_lat": 43.920240096152, + "southest_lat": 42.68989270234, + "eastest_lon": 2.0478554672695, + "westest_lon": 0.44199364903152 + }, + { + "id": "32", + "name": "Gers", + "northest_lat": 44.078224550392, + "southest_lat": 43.310884111508, + "eastest_lon": 1.2013345895525, + "westest_lon": -0.28211623210758 + }, + { + "id": "33", + "name": "Gironde", + "northest_lat": 45.574691325999, + "southest_lat": 44.193811119459, + "eastest_lon": 0.31506020240148, + "westest_lon": -1.2617334302552 + }, + { + "id": "34", + "name": "Hérault", + "northest_lat": 43.969527164685, + "southest_lat": 43.212804132866, + "eastest_lon": 4.1944474773799, + "westest_lon": 2.5399656073586 + }, + { + "id": "35", + "name": "Ille-et-Vilaine", + "northest_lat": 48.704854504824, + "southest_lat": 47.631356309182, + "eastest_lon": -1.0168893967587, + "westest_lon": -2.289084836122 + }, + { + "id": "36", + "name": "Indre", + "northest_lat": 47.276819032313, + "southest_lat": 46.347214822447, + "eastest_lon": 2.2043920861378, + "westest_lon": 0.86746898682573 + }, + { + "id": "37", + "name": "Indre-et-Loire", + "northest_lat": 47.709346222795, + "southest_lat": 46.737086924375, + "eastest_lon": 1.3653663291974, + "westest_lon": 0.053277684947378 + }, + { + "id": "38", + "name": "Isère", + "northest_lat": 45.883269928025, + "southest_lat": 44.696067584965, + "eastest_lon": 6.3588423781754, + "westest_lon": 4.7441167394752 + }, + { + "id": "39", + "name": "Jura", + "northest_lat": 47.305475313171, + "southest_lat": 46.260731935709, + "eastest_lon": 6.2033299339615, + "westest_lon": 5.2548827302617 + }, + { + "id": "40", + "name": "Landes", + "northest_lat": 44.532195517275, + "southest_lat": 43.487949116697, + "eastest_lon": 0.13672631290526, + "westest_lon": -1.524870110434 + }, + { + "id": "41", + "name": "Loir-et-Cher", + "northest_lat": 48.132548568904, + "southest_lat": 47.18622172903, + "eastest_lon": 2.2478931361182, + "westest_lon": 0.58052041667909 + }, + { + "id": "42", + "name": "Loire", + "northest_lat": 46.275936357353, + "southest_lat": 45.232177394436, + "eastest_lon": 4.7604638818845, + "westest_lon": 3.6906909501902 + }, + { + "id": "43", + "name": "Haute-Loire", + "northest_lat": 45.427582294546, + "southest_lat": 44.743866105932, + "eastest_lon": 4.489606977621, + "westest_lon": 3.0822533822787 + }, + { + "id": "44", + "name": "Loire-Atlantique", + "northest_lat": 47.833557723029, + "southest_lat": 46.860078088448, + "eastest_lon": -0.94643916329696, + "westest_lon": -2.5589448655806 + }, + { + "id": "45", + "name": "Loiret", + "northest_lat": 48.344598562828, + "southest_lat": 47.482968445146, + "eastest_lon": 3.1284487900515, + "westest_lon": 1.5129691249084 + }, + { + "id": "46", + "name": "Lot", + "northest_lat": 45.046275852749, + "southest_lat": 44.204018679795, + "eastest_lon": 2.2108934010391, + "westest_lon": 0.98177646477517 + }, + { + "id": "47", + "name": "Lot-et-Garonne", + "northest_lat": 44.764390535693, + "southest_lat": 43.973860568873, + "eastest_lon": 1.0779367166615, + "westest_lon": -0.14068987994571 + }, + { + "id": "48", + "name": "Lozère", + "northest_lat": 44.971408091786, + "southest_lat": 44.113818175271, + "eastest_lon": 3.9981617468281, + "westest_lon": 2.981675726654 + }, + { + "id": "49", + "name": "Maine-et-Loire", + "northest_lat": 47.809992506553, + "southest_lat": 46.969397597368, + "eastest_lon": 0.23453049018557, + "westest_lon": -1.3541992398083 + }, + { + "id": "50", + "name": "Manche", + "northest_lat": 49.725557927402, + "southest_lat": 48.458282754255, + "eastest_lon": -0.73732101904671, + "westest_lon": -1.9472733176655 + }, + { + "id": "51", + "name": "Marne", + "northest_lat": 49.406179032892, + "southest_lat": 48.516108006569, + "eastest_lon": 5.0379027924329, + "westest_lon": 3.398657955437 + }, + { + "id": "52", + "name": "Haute-Marne", + "northest_lat": 48.688711618117, + "southest_lat": 47.576950536437, + "eastest_lon": 5.8908642780035, + "westest_lon": 4.6268310932286 + }, + { + "id": "53", + "name": "Mayenne", + "northest_lat": 48.567994064435, + "southest_lat": 47.733379704738, + "eastest_lon": -0.049909790963035, + "westest_lon": -1.238247803597 + }, + { + "id": "54", + "name": "Meurthe-et-Moselle", + "northest_lat": 49.562644003065, + "southest_lat": 48.349889737943, + "eastest_lon": 7.1231636635608, + "westest_lon": 5.429907860027 + }, + { + "id": "55", + "name": "Meuse", + "northest_lat": 49.617086785829, + "southest_lat": 48.410687855212, + "eastest_lon": 5.8541770017029, + "westest_lon": 4.8885820531146 + }, + { + "id": "56", + "name": "Morbihan", + "northest_lat": 48.210884763611, + "southest_lat": 47.283069445657, + "eastest_lon": -2.0357552590146, + "westest_lon": -3.7321436369252 + }, + { + "id": "57", + "name": "Moselle", + "northest_lat": 49.510019040716, + "southest_lat": 48.52694525177, + "eastest_lon": 7.6352815933424, + "westest_lon": 5.8934039932125 + }, + { + "id": "58", + "name": "Nièvre", + "northest_lat": 47.587958865747, + "southest_lat": 46.651760006926, + "eastest_lon": 4.2306617272065, + "westest_lon": 2.8451871650071 + }, + { + "id": "59", + "name": "Nord", + "northest_lat": 51.08854370897, + "southest_lat": 49.969186662527, + "eastest_lon": 4.2279959931456, + "westest_lon": 2.0677049871716 + }, + { + "id": "60", + "name": "Oise", + "northest_lat": 49.758309270134, + "southest_lat": 49.060452516659, + "eastest_lon": 3.162641421643, + "westest_lon": 1.6895744511517 + }, + { + "id": "61", + "name": "Orne", + "northest_lat": 48.972557592954, + "southest_lat": 48.181599665308, + "eastest_lon": 0.9762713097259, + "westest_lon": -0.86036021134895 + }, + { + "id": "62", + "name": "Pas-de-Calais", + "northest_lat": 51.006501514321, + "southest_lat": 50.020975633738, + "eastest_lon": 3.1883563131291, + "westest_lon": 1.5577948179294 + }, + { + "id": "63", + "name": "Puy-de-Dôme", + "northest_lat": 46.255486133208, + "southest_lat": 45.287121578381, + "eastest_lon": 3.9844000097893, + "westest_lon": 2.388014020679 + }, + { + "id": "64", + "name": "Pyrénées-Atlantiques", + "northest_lat": 43.596401195938, + "southest_lat": 42.777515930774, + "eastest_lon": 0.02629551293813, + "westest_lon": -1.7908870919282 + }, + { + "id": "65", + "name": "Hautes-Pyrénées", + "northest_lat": 43.609311711216, + "southest_lat": 42.674921018438, + "eastest_lon": 0.64553925526757, + "westest_lon": -0.3270823405503 + }, + { + "id": "66", + "name": "Pyrénées-Orientales", + "northest_lat": 42.918339638726, + "southest_lat": 42.33364688988, + "eastest_lon": 3.1747892794105, + "westest_lon": 1.7256472450279 + }, + { + "id": "67", + "name": "Bas-Rhin", + "northest_lat": 49.077884925649, + "southest_lat": 48.120371112534, + "eastest_lon": 8.2303986615424, + "westest_lon": 6.9403717864006 + }, + { + "id": "68", + "name": "Haut-Rhin", + "northest_lat": 48.310471263573, + "southest_lat": 47.422198455938, + "eastest_lon": 7.6220859200517, + "westest_lon": 6.8428287756472 + }, + { + "id": "69", + "name": "Rhône", + "northest_lat": 46.303994122044, + "southest_lat": 45.45503324486, + "eastest_lon": 5.1592030475156, + "westest_lon": 4.243469905983 + }, + { + "id": "70", + "name": "Haute-Saône", + "northest_lat": 48.023714993889, + "southest_lat": 47.253139353829, + "eastest_lon": 6.8235333222471, + "westest_lon": 5.3727580571009 + }, + { + "id": "71", + "name": "Saône-et-Loire", + "northest_lat": 47.155410810959, + "southest_lat": 46.156946471815, + "eastest_lon": 5.4622983197648, + "westest_lon": 3.6225898833129 + }, + { + "id": "72", + "name": "Sarthe", + "northest_lat": 48.482954540393, + "southest_lat": 47.569104805534, + "eastest_lon": 0.91379809767445, + "westest_lon": -0.44786007819229 + }, + { + "id": "73", + "name": "Savoie", + "northest_lat": 45.93845768164, + "southest_lat": 45.051837207667, + "eastest_lon": 7.1842712160815, + "westest_lon": 5.6230208703548 + }, + { + "id": "74", + "name": "Haute-Savoie", + "northest_lat": 46.408081546332, + "southest_lat": 45.682198985672, + "eastest_lon": 7.0438913499404, + "westest_lon": 5.8074048290847 + }, + { + "id": "75", + "name": "Paris", + "northest_lat": 48.902007785215, + "southest_lat": 48.816314210034, + "eastest_lon": 2.4675819883673, + "westest_lon": 2.2242191058804 + }, + { + "id": "76", + "name": "Seine-Maritime", + "northest_lat": 50.070851596042, + "southest_lat": 49.252262168305, + "eastest_lon": 1.79022549105, + "westest_lon": 0.065609431053556 + }, + { + "id": "77", + "name": "Seine-et-Marne", + "northest_lat": 49.11755332218, + "southest_lat": 48.122204261229, + "eastest_lon": 3.555613758785, + "westest_lon": 2.3931765378081 + }, + { + "id": "78", + "name": "Yvelines", + "northest_lat": 49.08303659502, + "southest_lat": 48.440146719021, + "eastest_lon": 2.2265538842831, + "westest_lon": 1.4472851104304 + }, + { + "id": "79", + "name": "Deux-Sèvres", + "northest_lat": 47.108333434925, + "southest_lat": 45.969661749473, + "eastest_lon": 0.22035828616308, + "westest_lon": -0.89196408624284 + }, + { + "id": "80", + "name": "Somme", + "northest_lat": 50.366290636763, + "southest_lat": 49.571762327, + "eastest_lon": 3.2030417908111, + "westest_lon": 1.3796981484469 + }, + { + "id": "81", + "name": "Tarn", + "northest_lat": 44.200834436147, + "southest_lat": 43.383508824887, + "eastest_lon": 2.93545676901, + "westest_lon": 1.5439759556659 + }, + { + "id": "82", + "name": "Tarn-et-Garonne", + "northest_lat": 44.393331095059, + "southest_lat": 43.770780304363, + "eastest_lon": 1.9963637896774, + "westest_lon": 0.73810974125492 + }, + { + "id": "83", + "name": "Var", + "northest_lat": 43.806770970879, + "southest_lat": 42.982043008785, + "eastest_lon": 6.9337211159516, + "westest_lon": 5.6559638228901 + }, + { + "id": "84", + "name": "Vaucluse", + "northest_lat": 44.431367227183, + "southest_lat": 43.658685905188, + "eastest_lon": 5.7573377215236, + "westest_lon": 4.649227423465 + }, + { + "id": "85", + "name": "Vendée", + "northest_lat": 47.083893903306, + "southest_lat": 46.266566974958, + "eastest_lon": -0.53779518169029, + "westest_lon": -2.3987614706025 + }, + { + "id": "86", + "name": "Vienne", + "northest_lat": 47.175754285742, + "southest_lat": 46.049008552161, + "eastest_lon": 1.2126877519811, + "westest_lon": -0.1021158452812 + }, + { + "id": "87", + "name": "Haute-Vienne", + "northest_lat": 46.401546863371, + "southest_lat": 45.437356928582, + "eastest_lon": 1.9094484810021, + "westest_lon": 0.62974117909144 + }, + { + "id": "88", + "name": "Vosges", + "northest_lat": 48.513587820739, + "southest_lat": 47.813051201983, + "eastest_lon": 7.1982872111206, + "westest_lon": 5.3944755711529 + }, + { + "id": "89", + "name": "Yonne", + "northest_lat": 48.39970411104, + "southest_lat": 47.312769315752, + "eastest_lon": 4.3403007872795, + "westest_lon": 2.8487899744432 + }, + { + "id": "90", + "name": "Territoire de Belfort", + "northest_lat": 47.824784354742, + "southest_lat": 47.433371743667, + "eastest_lon": 7.1398015507652, + "westest_lon": 6.7576409592057 + }, + { + "id": "91", + "name": "Essonne", + "northest_lat": 48.776101996393, + "southest_lat": 48.284688606385, + "eastest_lon": 2.5853737107586, + "westest_lon": 1.9149199821626 + }, + { + "id": "92", + "name": "Hauts-de-Seine", + "northest_lat": 48.950965864655, + "southest_lat": 48.72948996497, + "eastest_lon": 2.3363529889891, + "westest_lon": 2.1458760215967 + }, + { + "id": "93", + "name": "Seine-Saint-Denis", + "northest_lat": 49.012397786393, + "southest_lat": 48.807437551952, + "eastest_lon": 2.6025997962059, + "westest_lon": 2.2882536989787 + }, + { + "id": "94", + "name": "Val-de-Marne", + "northest_lat": 48.861405371284, + "southest_lat": 48.688326690701, + "eastest_lon": 2.6136517425679, + "westest_lon": 2.3102224901101 + }, + { + "id": "95", + "name": "Val-d'Oise", + "northest_lat": 49.232197221792, + "southest_lat": 48.908679329899, + "eastest_lon": 2.5905283926735, + "westest_lon": 1.608798807603 + }, + { + "id": "2A", + "name": "Corse-du-Sud", + "northest_lat": 42.381404942274, + "southest_lat": 41.362164776515, + "eastest_lon": 9.4073217319481, + "westest_lon": 8.5401025407511 + }, + { + "id": "2B", + "name": "Haute-Corse", + "northest_lat": 43.011724041684, + "southest_lat": 41.832143660252, + "eastest_lon": 9.5592262719626, + "westest_lon": 8.5734085639674 + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/average.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/average.svg new file mode 100644 index 00000000000..f690930b938 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/average.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/bad.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/bad.svg new file mode 100644 index 00000000000..aff876d3508 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/bad.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/degrated.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/degrated.svg new file mode 100644 index 00000000000..df239efc873 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/degrated.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/extremely-bad.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/extremely-bad.svg new file mode 100644 index 00000000000..bce1d6ef083 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/extremely-bad.svg @@ -0,0 +1,6 @@ + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/good.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/good.svg new file mode 100644 index 00000000000..22a5d524ec6 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/good.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/pollen.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/pollen.svg new file mode 100644 index 00000000000..1d75a230199 --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/pollen.svg @@ -0,0 +1,4 @@ + + +