diff --git a/CODEOWNERS b/CODEOWNERS
index 97bd8089e8d..3b362ef30a7 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 ea798c3d2c6..a06ad5acd05 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..a66ce2b105f
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/NOTICE
@@ -0,0 +1,27 @@
+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
+
+== Credits
+Icon set coming from the noon project:
+Hazel, Ash : Imogen Oh
+Birch, Oak : monkik
+Cypress, Alder : Levi
+Poplar, Rumex : Laymik
+Willow : PizzaOter
+Hornbeam, Linden : Cannavale
+Olive : BnB Studio
+Chestnut : Muhammad Fadli Rusady
+Plantain : Lars Meiertoberens
+Grasses : Neneng Yuliani Lestari
+Ragweed, Wormwood : bsd studio
diff --git a/bundles/org.openhab.binding.airparif/README.md b/bundles/org.openhab.binding.airparif/README.md
new file mode 100755
index 00000000000..0f0cd51e323
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/README.md
@@ -0,0 +1,225 @@
+# AirParif Binding
+
+This binding uses the [AirParif service](https://www.airparif.fr/) for providing air quality information for Paris and departments of the Ile-de-France.
+To use it, you first need to [register and get your API key](https://www.airparif.fr/interface-de-programmation-applicative).
+You'll receive your API Key by mail.
+
+## Supported Things
+
+- `api`: bridge used to connect to the AirParif service. Provides some general informations for the whole area.
+- `location`: Presents the pollen and air quality information for a given location.
+
+Of course, you can add multiple `location`s, e.g. for gathering pollen or air quality data for different locations.
+
+## Discovery
+
+Once your `api` bridge is created and configured with the API Key, a default `location` can be auto-discovered based on system location.
+It will be configured with the system location and detected department.
+
+## Thing Configuration
+
+### `api` Thing Configuration
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|-----------------------------------|---------|----------|----------|
+| apikey | text | Token used to access the service | N/A | yes | no |
+
+### `location` Thing Configuration
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|--------------------------------------------------------------------------------|---------|----------|----------|
+| location | text | Geo coordinates to be considered (as ,[,]) | N/A | yes | no |
+| department | text | Code of the department (two digits) (*) | N/A | yes | no |
+
+(*) When auto-discovered, the department will be pre-filled based on the location and bounding limits defined in the internal department database.
+Please check that proposed value is correct according to the place.
+
+## Channels
+
+### `api` Thing Channels
+
+| Group | Channel | Type | Read/Write | Description |
+|----------------------|----------------|----------------|------------|----------------------------------------------|
+| pollens | comment | String | R | Current pollens situation |
+| pollens | begin-validity | DateTime | R | Bulletin validity start |
+| pollens | end-validity | DateTime | R | Bulletin validity end |
+| aq-bulletin | comment | String | R | General message for the air quality bulletin |
+| aq-bulletin | no2-min | Number:Density | R | Minimum level of NO2 concentration |
+| aq-bulletin | no2-max | Number:Density | R | Maximum level of NO2 concentration |
+| aq-bulletin | o3-min | Number:Density | R | Minimum level of O3 concentration |
+| aq-bulletin | o3-max | Number:Density | R | Maximum level of O3 concentration |
+| aq-bulletin | pm10-min | Number:Density | R | Minimum level of PM 10 concentration |
+| aq-bulletin | pm10-max | Number:Density | R | Maximum level of PM 10 concentration |
+| aq-bulletin | pm25-min | Number:Density | R | Minimum level of PM 2.5 concentration |
+| aq-bulletin | pm25-max | Number:Density | R | Maximum level of PM 2.5 concentration |
+| aq-bulletin-tomorrow | comment | String | R | General message for the air quality bulletin |
+| aq-bulletin-tomorrow | no2-min | Number:Density | R | Minimum level of NO2 concentration |
+| aq-bulletin-tomorrow | no2-max | Number:Density | R | Maximum level of NO2 concentration |
+| aq-bulletin-tomorrow | o3-min | Number:Density | R | Minimum level of O3 concentration |
+| aq-bulletin-tomorrow | o3-max | Number:Density | R | Maximum level of O3 concentration |
+| aq-bulletin-tomorrow | pm10-min | Number:Density | R | Minimum level of PM 10 concentration |
+| aq-bulletin-tomorrow | pm10-max | Number:Density | R | Maximum level of PM 10 concentration |
+| aq-bulletin-tomorrow | pm25-min | Number:Density | R | Minimum level of PM 2.5 concentration |
+| aq-bulletin-tomorrow | pm25-max | Number:Density | R | Maximum level of PM 2.5 concentration |
+| daily | message | String | R | Today's daily general information |
+| daily | tomorrow | String | R | Tomorrow's daily general information |
+
+### `location` Thing Channels
+
+| Group | Channel | Type | Read/Write | Description |
+|---------|------------|----------------|------------|----------------------------------------------------------|
+| pollens | cypress | Number | R | Alert level associated to this taxon (*) |
+| pollens | hazel | Number | R | Alert level associated to this taxon (*) |
+| pollens | alder | Number | R | Alert level associated to this taxon (*) |
+| pollens | poplar | Number | R | Alert level associated to this taxon (*) |
+| pollens | willow | Number | R | Alert level associated to this taxon (*) |
+| pollens | ash | Number | R | Alert level associated to this taxon (*) |
+| pollens | hornbeam | Number | R | Alert level associated to this taxon (*) |
+| pollens | birch | Number | R | Alert level associated to this taxon (*) |
+| pollens | plane | Number | R | Alert level associated to this taxon (*) |
+| pollens | oak | Number | R | Alert level associated to this taxon (*) |
+| pollens | olive | Number | R | Alert level associated to this taxon (*) |
+| pollens | linden | Number | R | Alert level associated to this taxon (*) |
+| pollens | chestnut | Number | R | Alert level associated to this taxon (*) |
+| pollens | rumex | Number | R | Alert level associated to this taxon (*) |
+| pollens | grasses | Number | R | Alert level associated to this taxon (*) |
+| pollens | plantain | Number | R | Alert level associated to this taxon (*) |
+| pollens | urticaceae | Number | R | Alert level associated to this taxon (*) |
+| pollens | wormwood | Number | R | Alert level associated to this taxon (*) |
+| pollens | ragweed | Number | R | Alert level associated to this taxon (*) |
+| indice | message | String | R | Alert message associated to the value of the index |
+| indice | timestamp | DateTime | R | Timestamp of the evaluation |
+| indice | alert | Number | R | ATMO Index associated to highest pollutant concentration |
+| o3 | message | String | R | Polllutant concentration alert message |
+| o3 | value | Number:Density | R | Concentration of the given pollutant |
+| o3 | alert | Number | R | Alert Level associated to pollutant concentration (**) |
+| no2 | message | String | R | Polllutant concentration alert message |
+| no2 | value | Number:Density | R | Concentration of the given pollutant |
+| no2 | alert | Number | R | Alert Level associated to pollutant concentration (**) |
+| pm25 | message | String | R | Polllutant concentration alert message |
+| pm25 | value | Number:Density | R | Concentration of the given pollutant |
+| pm25 | alert | Number | R | Alert Level associated to pollutant concentration (**) |
+| pm10 | message | String | R | Polllutant concentration alert message |
+| pm10 | value | Number:Density | R | Concentration of the given pollutant |
+| pm10 | alert | Number | R | Alert Level associated to pollutant concentration (**) |
+
+(*) Each pollen alert level has an associated color and description:
+
+| Code | Color | Description |
+|------|--------|-----------------------|
+| 0 | Green | No allergic risk |
+| 1 | Yellow | Low allergic risk |
+| 2 | Orange | Average allergic risk |
+| 3 | Red | High allergic risk |
+
+(*) Each pollutant concentration is associated to an alert level (and an icon) :
+
+| Code | Description |
+|------|---------------|
+| 0 | Good |
+| 1 | Average |
+| 2 | Degrated |
+| 3 | Bad |
+| 4 | Very Bad |
+| 5 | Extremely Bad |
+
+## Provided icon set
+
+This binding has its own IconProvider and makes available the following list of icons
+
+| Icon Name | Dynamic | Illustration |
+|------------------------|---------|--------------|
+| oh:airparif:aq | Yes | ![](doc/images/aq.svg) |
+| oh:airparif:alder | Yes | ![](doc/images/alder.svg) |
+| oh:airparif:ash | Yes | ![](doc/images/ash.svg) |
+| oh:airparif:birch | Yes | ![](doc/images/birch.svg) |
+| oh:airparif:chestnut | Yes | ![](doc/images/chestnut.svg) |
+| oh:airparif:cypress | Yes | ![](doc/images/cypress.svg) |
+| oh:airparif:grasses | Yes | ![](doc/images/grasses.svg) |
+| oh:airparif:hazel | Yes | ![](doc/images/hazel.svg) |
+| oh:airparif:hornbeam | Yes | ![](doc/images/hornbeam.svg) |
+| oh:airparif:linden | Yes | ![](doc/images/linden.svg) |
+| oh:airparif:oak | Yes | ![](doc/images/oak.svg) |
+| oh:airparif:olive | Yes | ![](doc/images/olive.svg) |
+| oh:airparif:plane | Yes | ![](doc/images/plane.svg) |
+| oh:airparif:plantain | Yes | ![](doc/images/plantain.svg) |
+| oh:airparif:pollen | Yes | ![](doc/images/pollen.svg) |
+| oh:airparif:poplar | Yes | ![](doc/images/poplar.svg) |
+| oh:airparif:ragweed | Yes | ![](doc/images/ragweed.svg) |
+| oh:airparif:rumex | Yes | ![](doc/images/rumex.svg) |
+| oh:airparif:urticaceae | Yes | ![](doc/images/urticaceae.svg) |
+| oh:airparif:willow | Yes | ![](doc/images/willow.svg) |
+| oh:airparif:wormwood | Yes | ![](doc/images/wormwood.svg) |
+
+## Full Examplee
+
+### Thing Configurationn
+
+```java
+Bridge airparif:api:local "AirParif" [ apikey="xxxxx-dddd-cccc-4321-zzzzzzzzzzzzz" ] {
+ location 78 "Yvelines" [ department="78", location="52.639,1.8284" ]
+}
+```
+### Item Configurationn
+
+```java
+String AirParifPollensComment "Situation" {channel="airparif:api:local:pollens#comment"}
+DateTime AirParifPollensBeginValidity "Begin validity" {channel="airparif:api:local:pollens#begin-validity"}
+DateTime AirParifPollensEndValidity "End validity" {channel="airparif:api:local:pollens#end-validity"}
+String AirParifAqBulletinComment "Message" {channel="airparif:api:local:aq-bulletin#comment"}
+Number:Density AirParifAqBulletinNo2Min "No2 min" {channel="airparif:api:local:aq-bulletin#no2-min"}
+Number:Density AirParifAqBulletinNo2Max "No2 max" {channel="airparif:api:local:aq-bulletin#no2-max"}
+Number:Density AirParifAqBulletinO3Min "O3 min" {channel="airparif:api:local:aq-bulletin#o3-min"}
+Number:Density AirParifAqBulletinO3Max "O3 max" {channel="airparif:api:local:aq-bulletin#o3-max"}
+Number:Density AirParifAqBulletinPm10Min "Pm 10 min" {channel="airparif:api:local:aq-bulletin#pm10-min"}
+Number:Density AirParifAqBulletinPm10Max "Pm 10 max" {channel="airparif:api:local:aq-bulletin#pm10-max"}
+Number:Density AirParifAqBulletinPm25Min "Pm 2.5 min" {channel="airparif:api:local:aq-bulletin#pm25-min"}
+Number:Density AirParifAqBulletinPm25Max "Pm 2.5 max" {channel="airparif:api:local:aq-bulletin#pm25-max"}
+String AirParifAqBulletinTomorrowComment "Message" {channel="airparif:api:local:aq-bulletin-tomorrow#comment"}
+Number:Density AirParifAqBulletinTomorrowNo2Min "No2 min" {channel="airparif:api:local:aq-bulletin-tomorrow#no2-min"}
+Number:Density AirParifAqBulletinTomorrowNo2Max "No2 max" {channel="airparif:api:local:aq-bulletin-tomorrow#no2-max"}
+Number:Density AirParifAqBulletinTomorrowO3Min "O3 min" {channel="airparif:api:local:aq-bulletin-tomorrow#o3-min"}
+Number:Density AirParifAqBulletinTomorrowO3Max "O3 max" {channel="airparif:api:local:aq-bulletin-tomorrow#o3-max"}
+Number:Density AirParifAqBulletinTomorrowPm10Min "Pm 10 min" {channel="airparif:api:local:aq-bulletin-tomorrow#pm10-min"}
+Number:Density AirParifAqBulletinTomorrowPm10Max "Pm 10 max" {channel="airparif:api:local:aq-bulletin-tomorrow#pm10-max"}
+Number:Density AirParifAqBulletinTomorrowPm25Min "Pm 2.5 min" {channel="airparif:api:local:aq-bulletin-tomorrow#pm25-min"}
+Number:Density AirParifAqBulletinTomorrowPm25Max "Pm 2.5 max" {channel="airparif:api:local:aq-bulletin-tomorrow#pm25-max"}
+String AirParifDailyMessage "Message" {channel="airparif:api:local:daily#message"}
+String AirParifDailyTomorrow "Tomorrow" {channel="airparif:api:local:daily#tomorrow"}
+
+Number Yvelines_Pollens_Cypress "Cypress" {channel="airparif:location:local:78:pollens#cypress"}
+Number Yvelines_Pollens_Hazel "Hazel level" {channel="airparif:location:local:78:pollens#hazel"}
+Number Yvelines_Pollens_Alder "Alder" {channel="airparif:location:local:78:pollens#alder"}
+Number Yvelines_Pollens_Poplar "Poplar" {channel="airparif:location:local:78:pollens#poplar"}
+Number Yvelines_Pollens_Willow "Willow" {channel="airparif:location:local:78:pollens#willow"}
+Number Yvelines_Pollens_Ash "Ash" {channel="airparif:location:local:78:pollens#ash"}
+Number Yvelines_Pollens_Hornbeam "Hornbeam" {channel="airparif:location:local:78:pollens#hornbeam"}
+Number Yvelines_Pollens_Birch "Birch level" {channel="airparif:location:local:78:pollens#birch"}
+Number Yvelines_Pollens_Plane "Plane" {channel="airparif:location:local:78:pollens#plane"}
+Number Yvelines_Pollens_Oak "Oak" {channel="airparif:location:local:78:pollens#oak"}
+Number Yvelines_Pollens_Olive "Olive" {channel="airparif:location:local:78:pollens#olive"}
+Number Yvelines_Pollens_Linden "Linden" {channel="airparif:location:local:78:pollens#linden"}
+Number Yvelines_Pollens_Chestnut "Chestnut" {channel="airparif:location:local:78:pollens#chestnut"}
+Number Yvelines_Pollens_Rumex "Rumex" {channel="airparif:location:local:78:pollens#rumex"}
+Number Yvelines_Pollens_Grasses "Grasses" {channel="airparif:location:local:78:pollens#grasses"}
+Number Yvelines_Pollens_Plantain "Plantain" {channel="airparif:location:local:78:pollens#plantain"}
+Number Yvelines_Pollens_Urticaceae "Urticacea" {channel="airparif:location:local:78:pollens#urticaceae"}
+Number Yvelines_Pollens_Wormwood "Wormwood" {channel="airparif:location:local:78:pollens#wormwood"}
+Number Yvelines_Pollens_Ragweed "Ragweed" {channel="airparif:location:local:78:pollens#ragweed"}
+String Yvelines_Indice_Message "Message" {channel="airparif:location:local:78:indice#message"}
+DateTime Yvelines_Indice_Timestamp "Timestamp" {channel="airparif:location:local:78:indice#timestamp"}
+Number Yvelines_Indice_Alert "Index" {channel="airparif:location:local:78:indice#alert"}
+String Yvelines_O3_Message "Message" {channel="airparif:location:local:78:o3#message"}
+Number:Density Yvelines_O3_Value "Concentration" {channel="airparif:location:local:78:o3#value"}
+Number Yvelines_O3_Alert "Alert level" {channel="airparif:location:local:78:o3#alert"}
+String Yvelines_No2_Message "Message" {channel="airparif:location:local:78:no2#message"}
+Number:Density Yvelines_No2_Value "Concentration" {channel="airparif:location:local:78:no2#value"}
+Number Yvelines_No2_Alert "Alert level" {channel="airparif:location:local:78:no2#alert"}
+String Yvelines_Pm25_Message "Message" {channel="airparif:location:local:78:pm25#message"}
+Number:Density Yvelines_Pm25_Value "Concentration" {channel="airparif:location:local:78:pm25#value"}
+Number Yvelines_Pm25_Alert "Alert level" {channel="airparif:location:local:78:pm25#alert"}
+String Yvelines_Pm10_Message "Message" {channel="airparif:location:local:78:pm10#message"}
+Number:Density Yvelines_Pm10_Value "Concentration" {channel="airparif:location:local:78:pm10#value"}
+Number Yvelines_Pm10_Alert "Alert level" {channel="airparif:location:local:78:pm10#alert"}
+``
+
diff --git a/bundles/org.openhab.binding.airparif/doc/images/alder.svg b/bundles/org.openhab.binding.airparif/doc/images/alder.svg
new file mode 100755
index 00000000000..ea5fc88483e
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/alder.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/aq.svg b/bundles/org.openhab.binding.airparif/doc/images/aq.svg
new file mode 100755
index 00000000000..056c569a4a6
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/aq.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/ash.svg b/bundles/org.openhab.binding.airparif/doc/images/ash.svg
new file mode 100755
index 00000000000..9c10179746d
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/ash.svg
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/birch.svg b/bundles/org.openhab.binding.airparif/doc/images/birch.svg
new file mode 100755
index 00000000000..f8ff505f3b6
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/birch.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/chestnut.svg b/bundles/org.openhab.binding.airparif/doc/images/chestnut.svg
new file mode 100755
index 00000000000..8cb9360bf60
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/chestnut.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/cypress.svg b/bundles/org.openhab.binding.airparif/doc/images/cypress.svg
new file mode 100755
index 00000000000..0cc76819acd
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/cypress.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/grasses.svg b/bundles/org.openhab.binding.airparif/doc/images/grasses.svg
new file mode 100755
index 00000000000..ae6279fd100
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/grasses.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/hazel.svg b/bundles/org.openhab.binding.airparif/doc/images/hazel.svg
new file mode 100755
index 00000000000..e2dd22c17f3
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/hazel.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/hornbeam.svg b/bundles/org.openhab.binding.airparif/doc/images/hornbeam.svg
new file mode 100755
index 00000000000..3a7d995bfaf
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/hornbeam.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/linden.svg b/bundles/org.openhab.binding.airparif/doc/images/linden.svg
new file mode 100755
index 00000000000..c10749ecd98
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/linden.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/oak.svg b/bundles/org.openhab.binding.airparif/doc/images/oak.svg
new file mode 100755
index 00000000000..343163e1a04
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/oak.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/olive.svg b/bundles/org.openhab.binding.airparif/doc/images/olive.svg
new file mode 100755
index 00000000000..719798a0fbf
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/olive.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/plane.svg b/bundles/org.openhab.binding.airparif/doc/images/plane.svg
new file mode 100755
index 00000000000..ab64767b0f7
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/plane.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/plantain.svg b/bundles/org.openhab.binding.airparif/doc/images/plantain.svg
new file mode 100755
index 00000000000..740cf781f00
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/plantain.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/poplar.svg b/bundles/org.openhab.binding.airparif/doc/images/poplar.svg
new file mode 100755
index 00000000000..5cd534d8b33
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/poplar.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/ragweed.svg b/bundles/org.openhab.binding.airparif/doc/images/ragweed.svg
new file mode 100755
index 00000000000..0beffb9f6f5
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/ragweed.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/rumex.svg b/bundles/org.openhab.binding.airparif/doc/images/rumex.svg
new file mode 100755
index 00000000000..d3607e52a37
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/rumex.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/urticaceae.svg b/bundles/org.openhab.binding.airparif/doc/images/urticaceae.svg
new file mode 100755
index 00000000000..07c76f90127
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/urticaceae.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/willow.svg b/bundles/org.openhab.binding.airparif/doc/images/willow.svg
new file mode 100755
index 00000000000..bc52029973b
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/willow.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/doc/images/wormwood.svg b/bundles/org.openhab.binding.airparif/doc/images/wormwood.svg
new file mode 100755
index 00000000000..9c02ae1e2e8
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/doc/images/wormwood.svg
@@ -0,0 +1,13 @@
+
\ 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..355042dd5f6
--- /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
+ 5.0.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..decda6e9c28
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2010-2025 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");
+
+ // Channel group ids
+ public static final String GROUP_POLLENS = "pollens";
+ public static final String GROUP_DAILY = "daily";
+ public static final String GROUP_AQ_BULLETIN = "aq-bulletin";
+ public static final String GROUP_AQ_BULLETIN_TOMORROW = GROUP_AQ_BULLETIN + "-tomorrow";
+
+ // List of all Channel ids
+ public static final String CHANNEL_BEGIN_VALIDITY = "begin-validity";
+ public static final String CHANNEL_END_VALIDITY = "end-validity";
+ public static final String CHANNEL_COMMENT = "comment";
+ public static final String CHANNEL_MESSAGE = "message";
+ public static final String CHANNEL_TOMORROW = "tomorrow";
+ public static final String CHANNEL_TIMESTAMP = "timestamp";
+ public static final String CHANNEL_VALUE = "value";
+ public static final String CHANNEL_ALERT = "alert";
+
+ 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..68f4ea25595
--- /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-2025 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 AirParif 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..3403b9fe6ce
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifHandlerFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2010-2025 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.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 HttpClientFactory httpClientFactory;
+
+ @Activate
+ public AirParifHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+ final @Reference AirParifDeserializer deserializer) {
+ this.httpClientFactory = httpClientFactory;
+ 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, httpClientFactory.getCommonHttpClient(), 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..38180df2d0d
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifIconProvider.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2010-2025 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.binding.airparif.internal.api.AirParifApi.Appreciation;
+import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen;
+import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
+import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.ui.icon.IconProvider;
+import org.openhab.core.ui.icon.IconSet;
+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 NEUTRAL_COLOR = "#3d3c3c";
+ private static final String DEFAULT_LABEL = "AirParif Icons";
+ private static final String AQ_ICON = "aq";
+ private static final String DEFAULT_DESCRIPTION = "Icons illustrating air quality levels provided by AirParif";
+ private static final List POLLEN_ICONS = Pollen.AS_SET.stream().map(Pollen::name).map(String::toLowerCase)
+ .toList();
+
+ private final Logger logger = LoggerFactory.getLogger(AirParifIconProvider.class);
+ private final TranslationProvider i18nProvider;
+ private final Bundle bundle;
+
+ @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)
+ && (category.equals(AQ_ICON) || POLLEN_ICONS.contains(category)) ? 0 : null;
+ }
+
+ @Override
+ public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) {
+ int ordinal = -1;
+ try {
+ ordinal = state != null ? Integer.valueOf(state) : -1;
+ } catch (NumberFormatException ignore) {
+ }
+
+ String iconName = "icon/%s.svg".formatted(category);
+ if (category.equals(AQ_ICON) && ordinal != -1 && ordinal < Appreciation.values().length - 2) {
+ iconName = iconName.replace(".", "-%d.".formatted(ordinal));
+ }
+
+ URL iconResource = bundle.getEntry(iconName);
+
+ String result = "";
+ try (InputStream stream = iconResource.openStream()) {
+ result = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
+
+ if (POLLEN_ICONS.contains(category)) {
+ PollenAlertLevel alertLevel = PollenAlertLevel.valueOf(ordinal);
+ result = result.replaceAll(NEUTRAL_COLOR, alertLevel.color);
+ }
+ } catch (IOException e) {
+ logger.warn("Unable to load ressource '{}': {}", iconResource.getPath(), e.getMessage());
+ }
+
+ 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..1349f604af7
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifApi.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2010-2025 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_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();
+
+ 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"),
+ VERY_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..141325de531
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2010-2025 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.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.measure.Unit;
+
+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 org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+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(//
+ Instant expiration, //
+ @SerializedName("droits") Set scopes) {
+ }
+
+ public record Message(//
+ String fr, //
+ @Nullable String en) {
+ }
+
+ public record PollutantConcentration(//
+ Pollutant pollutant, //
+ int min, //
+ int max) {
+
+ private State getQuantity(int value) {
+ Unit> unit = pollutant.unit;
+ if (unit != null) {
+ return new QuantityType<>(value, unit);
+ }
+ return UnDefType.NULL;
+ }
+
+ public State getMin() {
+ return getQuantity(min);
+ }
+
+ public State getMax() {
+ return getQuantity(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 boolean isToday() {
+ return previsionDate.equals(LocalDate.now());
+ }
+ }
+
+ 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) {
+ }
+
+ public class PollensResponse {
+ private static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yy");
+ private static Pattern PATTERN = Pattern.compile("\\d{2}.\\d{2}.\\d{2}");
+ private static ZoneId DEFAULT_ZONE = ZoneId.of("Europe/Paris");
+
+ public List data = List.of();
+ private @Nullable Instant beginValidity;
+ private @Nullable Instant endValidity;
+
+ public Optional getData() {
+ return Optional.ofNullable(data.isEmpty() ? null : data.get(0));
+ }
+
+ private Set getValidities() {
+ Set validities = new TreeSet<>();
+ getData().ifPresent(pollens -> {
+ Matcher matcher = PATTERN.matcher(pollens.periode);
+ while (matcher.find()) {
+ validities.add(LocalDate.parse(matcher.group(), FORMATTER).atStartOfDay(DEFAULT_ZONE).toInstant());
+ }
+ });
+
+ return validities;
+ }
+
+ public Optional getBeginValidity() {
+ if (beginValidity == null) {
+ beginValidity = getValidities().iterator().next();
+ }
+ return Optional.ofNullable(beginValidity);
+ }
+
+ public Optional getEndValidity() {
+ if (endValidity == null) {
+ endValidity = getValidities().stream().reduce((prev, next) -> next).orElse(null);
+ }
+ return Optional.ofNullable(endValidity);
+ }
+
+ public Duration getValidityDuration() {
+ return Objects.requireNonNull(getEndValidity().map(end -> {
+ Duration duration = Duration.between(Instant.now(), end);
+ return duration.isNegative() ? Duration.ZERO : duration;
+ }).orElse(Duration.ZERO));
+ }
+
+ public Optional getComment() {
+ return getData().map(pollens -> pollens.commentaire);
+ }
+
+ public Map getDepartment(String id) {
+ Map result = new HashMap<>();
+ Optional donnees = getData();
+ if (donnees.isPresent()) {
+ Pollens depts = donnees.get();
+ PollenAlertLevel[] valeurs = depts.valeurs.get(id);
+ if (valeurs != null) {
+ for (int i = 0; i < valeurs.length; i++) {
+ result.put(depts.taxons[i], valeurs[i]);
+ }
+ }
+ }
+ return result;
+ }
+ }
+
+ public record Concentration(//
+ @SerializedName("polluant") Pollutant pollutant, //
+ Instant date, //
+ @SerializedName("valeurs") double[] values, //
+ @Nullable Message message) {
+
+ public State getMessage() {
+ return message != null ? new StringType(message.fr()) : UnDefType.NULL;
+ }
+
+ public State getQuantity() {
+ Unit> unit = pollutant.unit;
+ return unit != null ? new QuantityType<>(getValue(), unit) : UnDefType.NULL;
+ }
+
+ public State getDate() {
+ return new DateTimeType(date);
+ }
+
+ public double getValue() {
+ return values[0];
+ }
+
+ public int getAlertLevel() {
+ return pollutant.getAppreciation(getValue()).ordinal();
+ }
+ }
+
+ public record Route(//
+ @SerializedName("dateRequise") Instant requestedDate, //
+ double[][] longlats, //
+ @SerializedName("resultats") List concentrations, //
+ @Nullable Message[] messages) {
+
+ }
+
+ public record ItineraireResponse(@SerializedName("itineraires") Route[] routes) {
+ }
+}
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..2ca46e4ac2c
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/PollenAlertLevel.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2010-2025 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 java.util.Objects;
+
+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, "#3a8b2f"),
+ @SerializedName("1")
+ LOW(1, "#f9a825"),
+ @SerializedName("2")
+ AVERAGE(2, "#ef6c00"),
+ @SerializedName("3")
+ HIGH(3, "#b71c1c"),
+ UNKNOWN(-1, "#b3b3b3");
+
+ public static final EnumSet AS_SET = EnumSet.allOf(PollenAlertLevel.class);
+
+ public final int riskLevel;
+ public final String color;
+
+ PollenAlertLevel(int riskLevel, String color) {
+ this.riskLevel = riskLevel;
+ this.color = color;
+ }
+
+ public static PollenAlertLevel valueOf(int ordinal) {
+ return Objects
+ .requireNonNull(AS_SET.stream().filter(pal -> pal.riskLevel == ordinal).findFirst().orElse(UNKNOWN));
+ }
+}
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..ab9f0cac84e
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/Pollutant.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2010-2025 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 javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.airparif.internal.api.AirParifApi.Appreciation;
+import org.openhab.core.library.unit.Units;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Pollutant} enum lists all pollutants tracked by AirParif
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public enum Pollutant {
+ // Concentration thresholds per pollutant are available here:
+ // https://www.airparif.fr/sites/default/files/pdf/guide_calcul_nouvel_indice_fedeAtmo_14122020.pdf
+
+ @SerializedName("pm25")
+ PM25(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 10, 20, 25, 50, 75 }),
+
+ @SerializedName("pm10")
+ PM10(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 20, 40, 50, 100, 150 }),
+
+ @SerializedName("no2")
+ NO2(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 40, 90, 120, 230, 340 }),
+
+ @SerializedName("o3")
+ O3(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 50, 100, 130, 240, 380 }),
+
+ @SerializedName("so2")
+ SO2(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 100, 200, 350, 500, 750 }),
+
+ @SerializedName("indice")
+ INDICE(null, new int[] {}),
+
+ UNKNOWN(null, new int[] {});
+
+ public static final EnumSet AS_SET = EnumSet.allOf(Pollutant.class);
+
+ public final @Nullable Unit> unit;
+ private final int[] thresholds;
+
+ Pollutant(@Nullable Unit> unit, int[] thresholds) {
+ this.unit = unit;
+ this.thresholds = thresholds;
+ }
+
+ public static Pollutant safeValueOf(String searched) {
+ try {
+ return Pollutant.valueOf(searched);
+ } catch (IllegalArgumentException e) {
+ return Pollutant.UNKNOWN;
+ }
+ }
+
+ public boolean hasUnit() {
+ return unit != null;
+ }
+
+ public Appreciation getAppreciation(double concentration) {
+ if (thresholds.length == 0) {
+ return Appreciation.UNKNOWN;
+ }
+
+ for (int i = 0; i < thresholds.length; i++) {
+ if (concentration <= thresholds[i]) {
+ return Appreciation.values()[i];
+ }
+ }
+ return Appreciation.EXTREMELY_BAD;
+ }
+}
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..2e5d04a1928
--- /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-2025 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..9fae95ff7a1
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/config/LocationConfiguration.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2025 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 static final String DEPARTMENT = "department";
+
+ public int refresh = 10;
+ public String location = "";
+ public String department = "";
+}
diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java
new file mode 100644
index 00000000000..11cec499091
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/db/DepartmentDbService.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2010-2025 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.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
+ */
+
+@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) {
+ }
+
+ 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..1623de3f3b3
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/AirParifDeserializer.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2010-2025 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.Instant;
+import java.time.LocalDate;
+
+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.PollenAlertLevel;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+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() {
+ gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
+ .registerTypeAdapter(PollenAlertLevel.class, new PollenAlertLevelDeserializer())
+ .registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory())
+ .registerTypeAdapter(PollutantConcentration.class, new PollutantConcentrationDeserializer())
+ .registerTypeAdapter(LocalDate.class,
+ (JsonDeserializer) (json, type, context) -> LocalDate
+ .parse(json.getAsJsonPrimitive().getAsString()))
+ .registerTypeAdapter(Instant.class, (JsonDeserializer) (json, type, context) -> {
+ String string = json.getAsJsonPrimitive().getAsString();
+ string += string.contains("+") ? "" : "Z";
+ return Instant.parse(string);
+ }).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'", e.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java
new file mode 100644
index 00000000000..5ba327b548e
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollenAlertLevelDeserializer.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2010-2025 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..ced52560e79
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/deserialization/PollutantConcentrationDeserializer.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2010-2025 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 {
+ private final Logger logger = LoggerFactory.getLogger(PollutantConcentrationDeserializer.class);
+
+ @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) {
+ logger.debug("Error deserializing PollutantConcentration: {}", json.toString());
+ }
+ }
+ 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..7dbf563f147
--- /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-2025 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..f33170003fd
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/discovery/AirParifDiscoveryService.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2010-2025 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.LOCATION_THING_TYPE;
+
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.airparif.internal.config.LocationConfiguration;
+import org.openhab.binding.airparif.internal.db.DepartmentDbService;
+import org.openhab.binding.airparif.internal.db.DepartmentDbService.Department;
+import org.openhab.binding.airparif.internal.handler.AirParifBridgeHandler;
+import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.i18n.LocationProvider;
+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 final DepartmentDbService dbService = new DepartmentDbService();
+
+ private @NonNullByDefault({}) LocationProvider locationProvider;
+
+ public AirParifDiscoveryService() {
+ super(AirParifBridgeHandler.class, Set.of(LOCATION_THING_TYPE), DISCOVER_TIMEOUT_SECONDS);
+ }
+
+ @Reference(unbind = "-")
+ public void setLocationProvider(LocationProvider locationProvider) {
+ this.locationProvider = locationProvider;
+ }
+
+ @Override
+ public void startScan() {
+ logger.debug("Starting AirParif discovery scan");
+
+ LocationProvider localLocation = locationProvider;
+ PointType location = localLocation != null ? localLocation.getLocation() : null;
+ if (location == null) {
+ logger.warn("LocationProvider.getLocation() is not set -> Will not provide any discovery results");
+ return;
+ }
+
+ createDepartmentResults(location);
+ }
+
+ private void createDepartmentResults(PointType serverLocation) {
+ List candidates = dbService.getBounding(serverLocation);
+ ThingUID bridgeUID = thingHandler.getThing().getUID();
+ if (!candidates.isEmpty()) {
+ candidates.forEach(dep -> thingDiscovered(
+ DiscoveryResultBuilder.create(new ThingUID(LOCATION_THING_TYPE, bridgeUID, dep.id()))//
+ .withLabel("Air Quality Report: %s".formatted(dep.name())) //
+ .withProperty(LocationConfiguration.DEPARTMENT, dep.id()) //
+ .withProperty(LocationConfiguration.LOCATION, serverLocation.toFullString())//
+ .withRepresentationProperty(LocationConfiguration.DEPARTMENT) //
+ .withBridge(bridgeUID).build()));
+ }
+ }
+}
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..cc7ae374e9e
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (c) 2010-2025 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.*;
+import static org.openhab.binding.airparif.internal.api.AirParifApi.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+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.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.InputStreamContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpStatus.Code;
+import org.openhab.binding.airparif.internal.AirParifException;
+import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen;
+import org.openhab.binding.airparif.internal.api.AirParifDto.Bulletin;
+import org.openhab.binding.airparif.internal.api.AirParifDto.Episode;
+import org.openhab.binding.airparif.internal.api.AirParifDto.ItineraireResponse;
+import org.openhab.binding.airparif.internal.api.AirParifDto.KeyInfo;
+import org.openhab.binding.airparif.internal.api.AirParifDto.PollensResponse;
+import org.openhab.binding.airparif.internal.api.AirParifDto.Route;
+import org.openhab.binding.airparif.internal.api.AirParifDto.Version;
+import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
+import org.openhab.binding.airparif.internal.api.Pollutant;
+import org.openhab.binding.airparif.internal.config.BridgeConfiguration;
+import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer;
+import org.openhab.binding.airparif.internal.discovery.AirParifDiscoveryService;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelGroupUID;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+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 implements HandlerUtils {
+ private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
+ private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+ private static final String AQ_JOB = "Air Quality Bulletin";
+ private static final String POLLENS_JOB = "Pollens Update";
+ private static final String EPISODE_JOB = "Episode";
+
+ private final Logger logger = LoggerFactory.getLogger(AirParifBridgeHandler.class);
+ private final Map> jobs = new HashMap<>();
+ private final AirParifDeserializer deserializer;
+ private final HttpClient httpClient;
+
+ private BridgeConfiguration config = new BridgeConfiguration();
+ private @Nullable PollensResponse pollens;
+
+ 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;
+ }
+ updateStatus(ThingStatus.UNKNOWN);
+ scheduler.execute(this::initiateConnexion);
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Disposing the AirParif bridge handler.");
+ cleanJobs();
+ }
+
+ public synchronized String executeUri(URI uri, HttpMethod method, @Nullable String payload)
+ throws AirParifException {
+ logger.debug("executeUrl: {} ", uri);
+
+ Request request = httpClient.newRequest(uri).method(method).timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ .header(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON).header("X-Api-Key", config.apikey);
+
+ if (payload != null && HttpMethod.POST.equals(method)) {
+ InputStream stream = new ByteArrayInputStream(payload.getBytes(DEFAULT_CHARSET));
+ try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) {
+ request.content(inputStreamContentProvider, MediaType.APPLICATION_JSON);
+ }
+ logger.trace(" -with payload : {} ", payload);
+ }
+
+ try {
+ ContentResponse response = request.send();
+
+ 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: %s", request.getURI(), e.getMessage());
+ } 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, HttpMethod.GET, null);
+ return deserializer.deserialize(clazz, content);
+ }
+
+ public synchronized T executeUri(URI uri, Class clazz, String payload) throws AirParifException {
+ String content = executeUri(uri, HttpMethod.POST, payload);
+ return deserializer.deserialize(clazz, content);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("The AirParif bridge does not handle commands");
+ }
+
+ private void initiateConnexion() {
+ Version version;
+ try { // This is only intended to validate communication with the server
+ version = executeUri(VERSION_URI, Version.class);
+ } catch (AirParifException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ return;
+ }
+
+ KeyInfo keyInfo;
+ 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;
+ }
+
+ thing.setProperty("api-version", version.version());
+ thing.setProperty("key-expiration", keyInfo.expiration().toString());
+ thing.setProperty("scopes", keyInfo.scopes().stream().map(e -> e.name()).collect(Collectors.joining(",")));
+ logger.debug("The api key is valid until {}", keyInfo.expiration().toString());
+ updateStatus(ThingStatus.ONLINE);
+
+ ThingUID thingUID = thing.getUID();
+
+ schedule(POLLENS_JOB, () -> updatePollens(new ChannelGroupUID(thingUID, GROUP_POLLENS)), Duration.ofSeconds(1));
+ schedule(AQ_JOB, () -> updateDailyAQBulletin(new ChannelGroupUID(thingUID, GROUP_AQ_BULLETIN),
+ new ChannelGroupUID(thingUID, GROUP_AQ_BULLETIN_TOMORROW)), Duration.ofSeconds(2));
+ schedule(EPISODE_JOB, () -> updateEpisode(new ChannelGroupUID(thingUID, GROUP_DAILY)), Duration.ofSeconds(3));
+ }
+
+ private void updatePollens(ChannelGroupUID pollensGroupUID) {
+ PollensResponse localPollens;
+ try {
+ localPollens = executeUri(POLLENS_URI, PollensResponse.class);
+ } catch (AirParifException e) {
+ logger.warn("Error updating pollens data: {}", e.getMessage());
+ return;
+ }
+
+ updateState(new ChannelUID(pollensGroupUID, CHANNEL_COMMENT), Objects.requireNonNull(
+ localPollens.getComment().map(comment -> (State) new StringType(comment)).orElse(UnDefType.NULL)));
+ updateState(new ChannelUID(pollensGroupUID, CHANNEL_BEGIN_VALIDITY), Objects.requireNonNull(
+ localPollens.getBeginValidity().map(begin -> (State) new DateTimeType(begin)).orElse(UnDefType.NULL)));
+ updateState(new ChannelUID(pollensGroupUID, CHANNEL_END_VALIDITY), Objects.requireNonNull(
+ localPollens.getEndValidity().map(end -> (State) new DateTimeType(end)).orElse(UnDefType.NULL)));
+
+ long delay = localPollens.getValidityDuration().getSeconds();
+ // if delay is null, update in 3600 seconds
+ delay += delay == 0 ? 3600 : 60;
+ schedule(POLLENS_JOB, () -> updatePollens(pollensGroupUID), Duration.ofSeconds(delay));
+
+ // Send pollens information to childs
+ getThing().getThings().stream().map(Thing::getHandler).filter(LocationHandler.class::isInstance)
+ .map(LocationHandler.class::cast).forEach(locHand -> locHand.setPollens(localPollens));
+ pollens = localPollens;
+ }
+
+ private void updateDailyAQBulletin(ChannelGroupUID todayGroupUID, ChannelGroupUID tomorrowGroupUID) {
+ Bulletin bulletin;
+ try {
+ bulletin = executeUri(PREV_BULLETIN_URI, Bulletin.class);
+ } catch (AirParifException e) {
+ logger.warn("Error updating Air Quality Bulletin: {}", e.getMessage());
+ return;
+ }
+
+ Set.of(bulletin.today(), bulletin.tomorrow()).stream().forEach(aq -> {
+ ChannelGroupUID groupUID = aq.isToday() ? todayGroupUID : tomorrowGroupUID;
+ updateState(new ChannelUID(groupUID, CHANNEL_COMMENT),
+ !aq.available() ? UnDefType.NULL : new StringType(aq.bulletin().fr()));
+
+ aq.concentrations().forEach(measure -> {
+ Pollutant pollutant = measure.pollutant();
+ String cName = pollutant.name().toLowerCase() + "-";
+ updateState(new ChannelUID(groupUID, cName + "min"),
+ aq.available() ? measure.getMin() : UnDefType.NULL);
+ updateState(new ChannelUID(groupUID, cName + "max"),
+ aq.available() ? measure.getMax() : UnDefType.NULL);
+ });
+ });
+
+ logger.debug("Rescheduling daily air quality bulletin job tomorrow morning");
+ schedule(AQ_JOB, () -> updateDailyAQBulletin(todayGroupUID, tomorrowGroupUID), untilTomorrowMorning());
+ }
+
+ private void updateEpisode(ChannelGroupUID dailyGroupUID) {
+ Episode episode;
+ try {
+ episode = executeUri(EPISODES_URI, Episode.class);
+ } catch (AirParifException e) {
+ logger.warn("Error updating Episode: {}", e.getMessage());
+ return;
+ }
+
+ logger.debug("The episode is {}", episode);
+
+ updateState(new ChannelUID(dailyGroupUID, CHANNEL_MESSAGE), new StringType(episode.message().fr()));
+ updateState(new ChannelUID(dailyGroupUID, CHANNEL_TOMORROW), new StringType(episode.message().fr()));
+
+ schedule(EPISODE_JOB, () -> updateEpisode(dailyGroupUID), untilTomorrowMorning());
+ }
+
+ private Duration untilTomorrowMorning() {
+ return Duration.between(ZonedDateTime.now(),
+ ZonedDateTime.now().plusDays(1).truncatedTo(ChronoUnit.DAYS).plusMinutes(1));
+ }
+
+ public @Nullable Route getConcentrations(String location) {
+ String[] elements = location.split(",");
+ if (elements.length >= 2) {
+ String req = "{\"itineraires\": [{\"date\": \"%s\",\"longlats\": [[%s,%s]]}],\"polluants\": [\"indice\",\"no2\",\"o3\",\"pm25\",\"pm10\"]}";
+ req = req.formatted(LocalDateTime.now().truncatedTo(ChronoUnit.HOURS), elements[1], elements[0]);
+ try {
+ ItineraireResponse result = executeUri(HORAIR_URI, ItineraireResponse.class, req);
+ return result.routes()[0];
+ } catch (AirParifException e) {
+ logger.warn("Error getting detailed concentrations: {}", e.getMessage());
+ }
+ } else {
+ logger.warn("Wrong localisation as input : {}", location);
+ }
+ return null;
+ }
+
+ public Map requestPollens(String department) {
+ PollensResponse localPollens = pollens;
+ return localPollens != null ? localPollens.getDepartment(department) : Map.of();
+ }
+
+ @Override
+ public @Nullable Bridge getBridge() {
+ return super.getBridge();
+ }
+
+ @Override
+ public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+ super.updateStatus(status, statusDetail, description);
+ }
+
+ @Override
+ public ScheduledExecutorService getScheduler() {
+ return scheduler;
+ }
+
+ @Override
+ public Logger getLogger() {
+ return logger;
+ }
+
+ @Override
+ public Map> getJobs() {
+ return jobs;
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Set.of(AirParifDiscoveryService.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/HandlerUtils.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/HandlerUtils.java
new file mode 100644
index 00000000000..f018bf019fc
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/HandlerUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2010-2025 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 java.time.Duration;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.slf4j.Logger;
+
+/**
+ * The {@link HandlerUtils} defines and implements some common methods for Thing Handlers
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public interface HandlerUtils {
+ default @Nullable ScheduledFuture> cancelFuture(@Nullable ScheduledFuture> job) {
+ if (job != null && !job.isCancelled()) {
+ job.cancel(true);
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ default @Nullable T getBridgeHandler(Class clazz) {
+ Bridge bridge = getBridge();
+ if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
+ BridgeHandler bridgeHandler = bridge.getHandler();
+ if (bridgeHandler != null) {
+ if (bridgeHandler.getClass() == clazz) {
+ return (T) bridgeHandler;
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge");
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge");
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "");
+ }
+ return null;
+ }
+
+ default void schedule(String jobName, Runnable job, Duration duration) {
+ ScheduledFuture> result = getJobs().remove(jobName);
+
+ getLogger().debug("{} {} in {}", result != null ? "Rescheduled" : "Scheduling", jobName, duration);
+ if (result != null) {
+ cancelFuture(result);
+ }
+
+ getJobs().put(jobName, getScheduler().schedule(job, duration.getSeconds(), TimeUnit.SECONDS));
+ }
+
+ default void cleanJobs() {
+ getJobs().values().forEach(job -> cancelFuture(job));
+ getJobs().clear();
+ }
+
+ void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description);
+
+ @Nullable
+ Bridge getBridge();
+
+ ScheduledExecutorService getScheduler();
+
+ Logger getLogger();
+
+ Map> getJobs();
+}
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..7e6fcc1ae05
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2010-2025 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.*;
+
+import java.time.Duration;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen;
+import org.openhab.binding.airparif.internal.api.AirParifDto.Concentration;
+import org.openhab.binding.airparif.internal.api.AirParifDto.PollensResponse;
+import org.openhab.binding.airparif.internal.api.AirParifDto.Route;
+import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
+import org.openhab.binding.airparif.internal.api.Pollutant;
+import org.openhab.binding.airparif.internal.config.LocationConfiguration;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelGroupUID;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.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 implements HandlerUtils {
+ private static final String AQ_JOB = "Local Air Quality";
+
+ private final Logger logger = LoggerFactory.getLogger(LocationHandler.class);
+ private final Map> jobs = new HashMap<>();
+
+ private Map myPollens = Map.of();
+ private @Nullable LocationConfiguration config;
+
+ public LocationHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(LocationConfiguration.class);
+ updateStatus(ThingStatus.UNKNOWN);
+ schedule(AQ_JOB, this::getConcentrations, Duration.ofSeconds(2));
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Disposing the AirParif bridge handler.");
+ cleanJobs();
+ }
+
+ public void setPollens(PollensResponse pollens) {
+ LocationConfiguration local = config;
+ if (local != null) {
+ updatePollenChannels(pollens.getDepartment(local.department));
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ private void updatePollenChannels(Map pollens) {
+ ChannelGroupUID pollensUID = new ChannelGroupUID(thing.getUID(), GROUP_POLLENS);
+ myPollens = pollens;
+ pollens.forEach((pollen, level) -> updateState(new ChannelUID(pollensUID, pollen.name().toLowerCase()),
+ new DecimalType(level.ordinal())));
+ }
+
+ private void getConcentrations() {
+ AirParifBridgeHandler apiHandler = getBridgeHandler(AirParifBridgeHandler.class);
+ LocationConfiguration local = config;
+ long delay = 3600;
+ if (apiHandler != null && local != null) {
+ if (myPollens.isEmpty()) {
+ updatePollenChannels(apiHandler.requestPollens(local.department));
+ }
+
+ Route route = apiHandler.getConcentrations(local.location);
+ if (route != null) {
+ int maxAlert = route.concentrations().stream().filter(conc -> conc.pollutant().hasUnit())
+ .map(Concentration::getAlertLevel).max(Comparator.comparing(Integer::valueOf)).get();
+
+ route.concentrations().stream().forEach(concentration -> {
+ Pollutant pollutant = concentration.pollutant();
+ ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), pollutant.name().toLowerCase());
+ updateState(new ChannelUID(groupUID, CHANNEL_MESSAGE), concentration.getMessage());
+ if (!pollutant.hasUnit()) {
+ updateState(new ChannelUID(groupUID, CHANNEL_TIMESTAMP), concentration.getDate());
+ updateState(new ChannelUID(groupUID, CHANNEL_ALERT), new DecimalType(maxAlert));
+ } else {
+ updateState(new ChannelUID(groupUID, CHANNEL_VALUE), concentration.getQuantity());
+ updateState(new ChannelUID(groupUID, CHANNEL_ALERT),
+ new DecimalType(concentration.getAlertLevel()));
+ }
+ });
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } else {
+ delay = 10;
+ }
+ schedule(AQ_JOB, this::getConcentrations, Duration.ofSeconds(delay));
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("This thing does not handle commands");
+ }
+
+ @Override
+ public @Nullable Bridge getBridge() {
+ return super.getBridge();
+ }
+
+ @Override
+ public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+ super.updateStatus(status, statusDetail, description);
+ }
+
+ @Override
+ public ScheduledExecutorService getScheduler() {
+ return scheduler;
+ }
+
+ @Override
+ public Logger getLogger() {
+ return logger;
+ }
+
+ @Override
+ public Map> getJobs() {
+ return jobs;
+ }
+}
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..c6016d94731
--- /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..ef0d892ad5f
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties
@@ -0,0 +1,239 @@
+# add-on
+
+addon.airparif.name = AirParif Binding
+addon.airparif.description = Air Quality data and forecasts provided by AirParif.
+
+# thing types
+
+thing-type.airparif.api.label = AirParif API Portal
+thing-type.airparif.api.description = Bridge to the AirParif API Portal. In order to receive the data, you must register an account on https://www.airparif.fr/contact and receive your API token.
+thing-type.airparif.api.group.aq-bulletin.label = Today's Air Quality Bulletin
+thing-type.airparif.api.group.aq-bulletin-tomorrow.label = Tomorrow's Air Quality Bulletin
+thing-type.airparif.location.label = Department Report
+thing-type.airparif.location.description = AirParif air quality report for the given location
+thing-type.airparif.location.group.no2.label = NO2 Concentration
+thing-type.airparif.location.group.o3.label = Ozone Concentration
+thing-type.airparif.location.group.pm10.label = PM10 Concentration
+thing-type.airparif.location.group.pm25.label = PM2.5 Concentration
+
+# thing types config
+
+thing-type.config.airparif.api.apikey.label = API Key
+thing-type.config.airparif.api.apikey.description = Token used to access the service
+thing-type.config.airparif.location.department.label = Department
+thing-type.config.airparif.location.department.description = Code of the department
+thing-type.config.airparif.location.department.option.75 = Paris
+thing-type.config.airparif.location.department.option.77 = Seine et Marne
+thing-type.config.airparif.location.department.option.78 = Yvelines
+thing-type.config.airparif.location.department.option.91 = Essonne
+thing-type.config.airparif.location.department.option.92 = Hauts de Seine
+thing-type.config.airparif.location.department.option.93 = Seine Saint Denis
+thing-type.config.airparif.location.department.option.94 = Val de Marne
+thing-type.config.airparif.location.department.option.95 = Val D'Oise
+thing-type.config.airparif.location.location.label = Location
+
+# channel group types
+
+channel-group-type.airparif.air-quality-bulletin.label = Air Quality Bulletin
+channel-group-type.airparif.air-quality-bulletin.channel.comment.label = Message
+channel-group-type.airparif.air-quality-bulletin.channel.comment.description = General message for the air quality bulletin
+channel-group-type.airparif.air-quality-bulletin.channel.no2-max.label = NO2 Max
+channel-group-type.airparif.air-quality-bulletin.channel.no2-max.description = Maximum level of NO2 concentration
+channel-group-type.airparif.air-quality-bulletin.channel.no2-min.label = NO2 Min
+channel-group-type.airparif.air-quality-bulletin.channel.no2-min.description = Minimum level of NO2 concentration
+channel-group-type.airparif.air-quality-bulletin.channel.o3-max.label = O3 Max
+channel-group-type.airparif.air-quality-bulletin.channel.o3-max.description = Maximum level of O3 concentration
+channel-group-type.airparif.air-quality-bulletin.channel.o3-min.label = O3 Min
+channel-group-type.airparif.air-quality-bulletin.channel.o3-min.description = Minimum level of O3 concentration
+channel-group-type.airparif.air-quality-bulletin.channel.pm10-max.label = PM 10 Max
+channel-group-type.airparif.air-quality-bulletin.channel.pm10-max.description = Maximum level of PM 10 concentration
+channel-group-type.airparif.air-quality-bulletin.channel.pm10-min.label = PM 10 Min
+channel-group-type.airparif.air-quality-bulletin.channel.pm10-min.description = Minimum level of PM 10 concentration
+channel-group-type.airparif.air-quality-bulletin.channel.pm25-max.label = PM 2.5 Max
+channel-group-type.airparif.air-quality-bulletin.channel.pm25-max.description = Maximum level of PM 2.5 concentration
+channel-group-type.airparif.air-quality-bulletin.channel.pm25-min.label = PM 2.5 Min
+channel-group-type.airparif.air-quality-bulletin.channel.pm25-min.description = Minimum level of PM 2.5 concentration
+channel-group-type.airparif.bridge-pollens.label = Pollen information for the region
+channel-group-type.airparif.bridge-pollens.channel.begin-validity.label = Begin Validity
+channel-group-type.airparif.bridge-pollens.channel.begin-validity.description = Bulletin validity start
+channel-group-type.airparif.bridge-pollens.channel.comment.label = Situation
+channel-group-type.airparif.bridge-pollens.channel.comment.description = Current pollens situation
+channel-group-type.airparif.bridge-pollens.channel.end-validity.label = End Validity
+channel-group-type.airparif.bridge-pollens.channel.end-validity.description = Bulletin validity end
+channel-group-type.airparif.daily.label = Daily Region Information
+channel-group-type.airparif.daily.channel.message.label = Message
+channel-group-type.airparif.daily.channel.message.description = Today's daily general information
+channel-group-type.airparif.daily.channel.tomorrow.label = Tomorrow
+channel-group-type.airparif.daily.channel.tomorrow.description = Tomorrow's daily general information
+channel-group-type.airparif.dept-pollens.label = Department Pollen Information
+channel-group-type.airparif.pollutant-mpc.label = Pollutant Concentration Information
+channel-group-type.airparif.pollutant-mpc.channel.alert.label = Alert Level
+channel-group-type.airparif.pollutant-mpc.channel.alert.description = Alert Level associated to pollutant concentration
+channel-group-type.airparif.pollutant-mpc.channel.message.label = Message
+channel-group-type.airparif.pollutant-mpc.channel.message.description = Polllutant concentration alert message
+channel-group-type.airparif.pollutant-mpc.channel.value.label = Concentration
+channel-group-type.airparif.pollutant-mpc.channel.value.description = Concentration of the given pollutant
+channel-group-type.airparif.pollutant-ndx.label = ATMO Index
+channel-group-type.airparif.pollutant-ndx.channel.alert.label = Index
+channel-group-type.airparif.pollutant-ndx.channel.alert.description = ATMO Index associated to highest pollutant concentration
+channel-group-type.airparif.pollutant-ndx.channel.message.label = Message
+channel-group-type.airparif.pollutant-ndx.channel.message.description = Alert message associated to the value of the index
+channel-group-type.airparif.pollutant-ndx.channel.timestamp.label = Timestamp
+channel-group-type.airparif.pollutant-ndx.channel.timestamp.description = Timestamp of the evaluation
+
+# channel types
+
+channel-type.airparif.alder-level.label = Alder
+channel-type.airparif.alder-level.state.option.0 = None
+channel-type.airparif.alder-level.state.option.1 = Low
+channel-type.airparif.alder-level.state.option.2 = Average
+channel-type.airparif.alder-level.state.option.3 = High
+channel-type.airparif.appreciation.label = Air Quality
+channel-type.airparif.appreciation.state.option.0 = Good
+channel-type.airparif.appreciation.state.option.1 = Average
+channel-type.airparif.appreciation.state.option.2 = Degrated
+channel-type.airparif.appreciation.state.option.3 = Bad
+channel-type.airparif.appreciation.state.option.4 = Very Bad
+channel-type.airparif.appreciation.state.option.5 = Extremely Bad
+channel-type.airparif.ash-level.label = Ash
+channel-type.airparif.ash-level.state.option.0 = None
+channel-type.airparif.ash-level.state.option.1 = Low
+channel-type.airparif.ash-level.state.option.2 = Average
+channel-type.airparif.ash-level.state.option.3 = High
+channel-type.airparif.birch-level.label = Birch Level
+channel-type.airparif.birch-level.state.option.0 = None
+channel-type.airparif.birch-level.state.option.1 = Low
+channel-type.airparif.birch-level.state.option.2 = Average
+channel-type.airparif.birch-level.state.option.3 = High
+channel-type.airparif.chestnut-level.label = Chestnut
+channel-type.airparif.chestnut-level.state.option.0 = None
+channel-type.airparif.chestnut-level.state.option.1 = Low
+channel-type.airparif.chestnut-level.state.option.2 = Average
+channel-type.airparif.chestnut-level.state.option.3 = High
+channel-type.airparif.comment.label = Comment
+channel-type.airparif.cypress-level.label = Cypress
+channel-type.airparif.cypress-level.state.option.0 = None
+channel-type.airparif.cypress-level.state.option.1 = Low
+channel-type.airparif.cypress-level.state.option.2 = Average
+channel-type.airparif.cypress-level.state.option.3 = High
+channel-type.airparif.grasses-level.label = Grasses
+channel-type.airparif.grasses-level.state.option.0 = None
+channel-type.airparif.grasses-level.state.option.1 = Low
+channel-type.airparif.grasses-level.state.option.2 = Average
+channel-type.airparif.grasses-level.state.option.3 = High
+channel-type.airparif.hazel-level.label = Hazel Level
+channel-type.airparif.hazel-level.state.option.0 = None
+channel-type.airparif.hazel-level.state.option.1 = Low
+channel-type.airparif.hazel-level.state.option.2 = Average
+channel-type.airparif.hazel-level.state.option.3 = High
+channel-type.airparif.hornbeam-level.label = Hornbeam
+channel-type.airparif.hornbeam-level.state.option.0 = None
+channel-type.airparif.hornbeam-level.state.option.1 = Low
+channel-type.airparif.hornbeam-level.state.option.2 = Average
+channel-type.airparif.hornbeam-level.state.option.3 = High
+channel-type.airparif.linden-level.label = Linden
+channel-type.airparif.linden-level.state.option.0 = None
+channel-type.airparif.linden-level.state.option.1 = Low
+channel-type.airparif.linden-level.state.option.2 = Average
+channel-type.airparif.linden-level.state.option.3 = High
+channel-type.airparif.mpc-value.label = Measure
+channel-type.airparif.ndx-value.label = Measure
+channel-type.airparif.oak-level.label = Oak
+channel-type.airparif.oak-level.state.option.0 = None
+channel-type.airparif.oak-level.state.option.1 = Low
+channel-type.airparif.oak-level.state.option.2 = Average
+channel-type.airparif.oak-level.state.option.3 = High
+channel-type.airparif.olive-level.label = Olive
+channel-type.airparif.olive-level.state.option.0 = None
+channel-type.airparif.olive-level.state.option.1 = Low
+channel-type.airparif.olive-level.state.option.2 = Average
+channel-type.airparif.olive-level.state.option.3 = High
+channel-type.airparif.plane-level.label = Plane
+channel-type.airparif.plane-level.state.option.0 = None
+channel-type.airparif.plane-level.state.option.1 = Low
+channel-type.airparif.plane-level.state.option.2 = Average
+channel-type.airparif.plane-level.state.option.3 = High
+channel-type.airparif.plantain-level.label = Plantain
+channel-type.airparif.plantain-level.state.option.0 = None
+channel-type.airparif.plantain-level.state.option.1 = Low
+channel-type.airparif.plantain-level.state.option.2 = Average
+channel-type.airparif.plantain-level.state.option.3 = High
+channel-type.airparif.poplar-level.label = Poplar
+channel-type.airparif.poplar-level.state.option.0 = None
+channel-type.airparif.poplar-level.state.option.1 = Low
+channel-type.airparif.poplar-level.state.option.2 = Average
+channel-type.airparif.poplar-level.state.option.3 = High
+channel-type.airparif.ragweed-level.label = Ragweed
+channel-type.airparif.ragweed-level.state.option.0 = None
+channel-type.airparif.ragweed-level.state.option.1 = Low
+channel-type.airparif.ragweed-level.state.option.2 = Average
+channel-type.airparif.ragweed-level.state.option.3 = High
+channel-type.airparif.rumex-level.label = Rumex
+channel-type.airparif.rumex-level.state.option.0 = None
+channel-type.airparif.rumex-level.state.option.1 = Low
+channel-type.airparif.rumex-level.state.option.2 = Average
+channel-type.airparif.rumex-level.state.option.3 = High
+channel-type.airparif.timestamp.label = Timestamp
+channel-type.airparif.urticaceae-level.label = Urticacea
+channel-type.airparif.urticaceae-level.state.option.0 = None
+channel-type.airparif.urticaceae-level.state.option.1 = Low
+channel-type.airparif.urticaceae-level.state.option.2 = Average
+channel-type.airparif.urticaceae-level.state.option.3 = High
+channel-type.airparif.willow-level.label = Willow
+channel-type.airparif.willow-level.state.option.0 = None
+channel-type.airparif.willow-level.state.option.1 = Low
+channel-type.airparif.willow-level.state.option.2 = Average
+channel-type.airparif.willow-level.state.option.3 = High
+channel-type.airparif.wormwood-level.label = Wormwood
+channel-type.airparif.wormwood-level.state.option.0 = None
+channel-type.airparif.wormwood-level.state.option.1 = Low
+channel-type.airparif.wormwood-level.state.option.2 = Average
+channel-type.airparif.wormwood-level.state.option.3 = High
+
+# channel group types
+
+channel-group-type.airparif.pollutant-mpc.channel.timestamp.label = Timestamp
+channel-group-type.airparif.pollutant-mpc.channel.timestamp.description = Timestamp of the measure
+channel-group-type.airparif.pollutant-ndx.channel.value.label = Value
+channel-group-type.airparif.pollutant-ndx.channel.value.description = Value of the global Index
+channel-group-type.airparif.pollutant-ppb.label = Pollutant Concentration Information
+channel-group-type.airparif.pollutant-ppb.channel.message.label = Message
+channel-group-type.airparif.pollutant-ppb.channel.message.description = Polllutant concentration alert message
+channel-group-type.airparif.pollutant-ppb.channel.timestamp.label = Timestamp
+channel-group-type.airparif.pollutant-ppb.channel.timestamp.description = Timestamp of the measure
+channel-group-type.airparif.pollutant-ppb.channel.value.label = Concentration
+channel-group-type.airparif.pollutant-ppb.channel.value.description = Concentration of the given pollutant
+
+# channel types
+
+channel-type.airparif.ppb-value.label = Measure
+
+# thing types
+
+thing-type.airparif.location.channel.end-validity.label = End Of Validity
+thing-type.airparif.location.channel.end-validity.description = Current bulletin validity ending
+
+# channel group types
+
+channel-group-type.airparif.pollens-group.label = Pollen information for the region
+channel-group-type.airparif.pollens-group.channel.begin-validity.label = Begin Validity
+channel-group-type.airparif.pollens-group.channel.begin-validity.description = Current bulletin validity start
+channel-group-type.airparif.pollens-group.channel.comment.label = Begin Validity
+channel-group-type.airparif.pollens-group.channel.comment.description = Current bulletin validity start
+channel-group-type.airparif.pollens-group.channel.end-validity.label = End Validity
+channel-group-type.airparif.pollens-group.channel.end-validity.description = Current bulletin validity ending
+
+# 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
+incorrect-bridge = Wrong bridge type
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml
new file mode 100755
index 00000000000..416faef5ed3
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ 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/channel-groups.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml
new file mode 100644
index 00000000000..fb2bf70230c
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+ Bulletin validity start
+
+
+
+ Bulletin validity end
+
+
+
+
+
+
+
+
+
+
+ Minimum level of NO2 concentration
+
+
+
+ Maximum level of NO2 concentration
+
+
+
+ Minimum level of O3 concentration
+
+
+
+ Maximum level of O3 concentration
+
+
+
+ Minimum level of PM 10 concentration
+
+
+
+ Maximum level of PM 10 concentration
+
+
+
+ Minimum level of PM 2.5 concentration
+
+
+
+ Maximum level of PM 2.5 concentration
+
+
+
+
+
+
+
+
+
+ Polllutant concentration alert message
+
+
+
+ Concentration of the given pollutant
+
+
+
+ Alert Level associated to pollutant concentration
+
+
+
+
+
+
+
+
+
+ Alert message associated to the value of the index
+
+
+
+ Timestamp of the evaluation
+
+
+
+ ATMO Index associated to highest pollutant concentration
+
+
+
+
+
+
+
+
+
+ Today's daily general information
+
+
+
+ Tomorrow's daily general information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..18810b36d3f
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml
@@ -0,0 +1,323 @@
+
+
+
+
+ DateTime
+
+ time
+
+
+
+
+
+
+
+ DateTime
+
+ @text/timestampChannelDescription
+ time
+
+
+
+
+ Number
+
+ oh:airparif:hazel
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:birch
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:cypress
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:alder
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:poplar
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:ash
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:olive
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:urticaceae
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:wormwood
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:rumex
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:ragweed
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:grasses
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:plantain
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:chestnut
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:oak
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:linden
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:plane
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:hornbeam
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ oh:airparif:willow
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number:Density
+
+
+
+
+
+ Number
+
+ oh:airparif:aq
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/location.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/location.xml
new file mode 100755
index 00000000000..aab4dd83b7f
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/location.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+ AirParif air quality report for the given location
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ department
+
+
+
+ location
+
+
+
+
+ Code of the department
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
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/alder.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/alder.svg
new file mode 100755
index 00000000000..ea5fc88483e
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/alder.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-0.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-0.svg
new file mode 100755
index 00000000000..80e94fe7fd2
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-0.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-1.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-1.svg
new file mode 100755
index 00000000000..2e78da14543
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-1.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-2.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-2.svg
new file mode 100755
index 00000000000..a7b3aa5ac8f
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-2.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-3.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-3.svg
new file mode 100755
index 00000000000..547740458c9
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-3.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-4.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-4.svg
new file mode 100755
index 00000000000..8db5142887d
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-4.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-5.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-5.svg
new file mode 100755
index 00000000000..80b94df18ba
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq-5.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq.svg
new file mode 100755
index 00000000000..056c569a4a6
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/aq.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/ash.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ash.svg
new file mode 100755
index 00000000000..9c10179746d
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ash.svg
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/birch.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/birch.svg
new file mode 100755
index 00000000000..f8ff505f3b6
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/birch.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/chestnut.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/chestnut.svg
new file mode 100755
index 00000000000..8cb9360bf60
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/chestnut.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/cypress.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/cypress.svg
new file mode 100755
index 00000000000..0cc76819acd
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/cypress.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/grasses.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/grasses.svg
new file mode 100755
index 00000000000..ae6279fd100
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/grasses.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/hazel.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hazel.svg
new file mode 100755
index 00000000000..e2dd22c17f3
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hazel.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/hornbeam.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hornbeam.svg
new file mode 100755
index 00000000000..3a7d995bfaf
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/hornbeam.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/linden.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/linden.svg
new file mode 100755
index 00000000000..c10749ecd98
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/linden.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/oak.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/oak.svg
new file mode 100755
index 00000000000..343163e1a04
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/oak.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/olive.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/olive.svg
new file mode 100755
index 00000000000..719798a0fbf
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/olive.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/plane.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plane.svg
new file mode 100755
index 00000000000..ab64767b0f7
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plane.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/plantain.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plantain.svg
new file mode 100755
index 00000000000..740cf781f00
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/plantain.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/poplar.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/poplar.svg
new file mode 100755
index 00000000000..5cd534d8b33
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/poplar.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/ragweed.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ragweed.svg
new file mode 100755
index 00000000000..0beffb9f6f5
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/ragweed.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/rumex.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/rumex.svg
new file mode 100755
index 00000000000..d3607e52a37
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/rumex.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/urticaceae.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/urticaceae.svg
new file mode 100755
index 00000000000..07c76f90127
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/urticaceae.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/willow.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/willow.svg
new file mode 100755
index 00000000000..bc52029973b
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/willow.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/icon/wormwood.svg b/bundles/org.openhab.binding.airparif/src/main/resources/icon/wormwood.svg
new file mode 100755
index 00000000000..9c02ae1e2e8
--- /dev/null
+++ b/bundles/org.openhab.binding.airparif/src/main/resources/icon/wormwood.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 6fed251e593..cc3bc2e80eb 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -48,6 +48,7 @@
org.openhab.binding.adorne
org.openhab.binding.ahawastecollection
org.openhab.binding.airgradient
+ org.openhab.binding.airparif
org.openhab.binding.airq
org.openhab.binding.airquality
org.openhab.binding.airvisualnode