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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@
+