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 @@ + + + + + + + + + Current pollens situation + + + + Bulletin validity start + + + + Bulletin validity end + + + + + + + + + + General message for the air quality bulletin + + + + 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 + + + + + String + + text + + + + + + 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