mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
Initial commit
Signed-off-by: Gaël L'hopital <gael@lhopital.org>
This commit is contained in:
parent
9544767641
commit
1265419834
@ -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
|
||||
|
@ -66,6 +66,11 @@
|
||||
<artifactId>org.openhab.binding.airgradient</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.airparif</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.airq</artifactId>
|
||||
|
13
bundles/org.openhab.binding.airparif/NOTICE
Executable file
13
bundles/org.openhab.binding.airparif/NOTICE
Executable file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
94
bundles/org.openhab.binding.airparif/README.md
Executable file
94
bundles/org.openhab.binding.airparif/README.md
Executable file
@ -0,0 +1,94 @@
|
||||
# AirParif Binding
|
||||
|
||||
_Give some details about what this binding is meant for - a protocol, system, specific device._
|
||||
|
||||
_If possible, provide some resources like pictures (only PNG is supported currently), a video, etc. to give an impression of what can be done with this binding._
|
||||
_You can place such resources into a `doc` folder next to this README.md._
|
||||
|
||||
_Put each sentence in a separate line to improve readability of diffs._
|
||||
|
||||
## Supported Things
|
||||
|
||||
_Please describe the different supported things / devices including their ThingTypeUID within this section._
|
||||
_Which different types are supported, which models were tested etc.?_
|
||||
_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._
|
||||
|
||||
- `bridge`: Short description of the Bridge, if any
|
||||
- `sample`: Short description of the Thing with the ThingTypeUID `sample`
|
||||
|
||||
## Discovery
|
||||
|
||||
_Describe the available auto-discovery features here._
|
||||
_Mention for what it works and what needs to be kept in mind when using it._
|
||||
|
||||
## Binding Configuration
|
||||
|
||||
_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```<bindingId>.cfg``` inside it._
|
||||
_In this section, you should link to this file and provide some information about the options._
|
||||
_The file could e.g. look like:_
|
||||
|
||||
```
|
||||
# Configuration for the AirParif Binding
|
||||
#
|
||||
# Default secret key for the pairing of the AirParif Thing.
|
||||
# It has to be between 10-40 (alphanumeric) characters.
|
||||
# This may be changed by the user for security reasons.
|
||||
secret=openHABSecret
|
||||
```
|
||||
|
||||
_Note that it is planned to generate some part of this based on the information that is available within ```src/main/resources/OH-INF/binding``` of your binding._
|
||||
|
||||
_If your binding does not offer any generic configurations, you can remove this section completely._
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
_Describe what is needed to manually configure a thing, either through the UI or via a thing-file._
|
||||
_This should be mainly about its mandatory and optional configuration parameters._
|
||||
|
||||
_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._
|
||||
|
||||
### `sample` Thing Configuration
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-----------------|---------|---------------------------------------|---------|----------|----------|
|
||||
| hostname | text | Hostname or IP address of the device | N/A | yes | no |
|
||||
| password | text | Password to access the device | N/A | yes | no |
|
||||
| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes |
|
||||
|
||||
## Channels
|
||||
|
||||
_Here you should provide information about available channel types, what their meaning is and how they can be used._
|
||||
|
||||
_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._
|
||||
|
||||
| Channel | Type | Read/Write | Description |
|
||||
|---------|--------|------------|-----------------------------|
|
||||
| control | Switch | RW | This is the control channel |
|
||||
|
||||
## Full Example
|
||||
|
||||
_Provide a full usage example based on textual configuration files._
|
||||
_*.things, *.items examples are mandatory as textual configuration is well used by many users._
|
||||
_*.sitemap examples are optional._
|
||||
|
||||
### Thing Configuration
|
||||
|
||||
```java
|
||||
Example thing configuration goes here.
|
||||
```
|
||||
### Item Configuration
|
||||
|
||||
```java
|
||||
Example item configuration goes here.
|
||||
```
|
||||
|
||||
### Sitemap Configuration
|
||||
|
||||
```perl
|
||||
Optional Sitemap configuration goes here.
|
||||
Remove this section, if not needed.
|
||||
```
|
||||
|
||||
## Any custom content here!
|
||||
|
||||
_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_
|
29
bundles/org.openhab.binding.airparif/doc/info.txt
Normal file
29
bundles/org.openhab.binding.airparif/doc/info.txt
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
Miguel Narvaez Miguel.Narvaez@airparif.fr via improvmx-mails.com
|
||||
30 sept. 2024 10:06 (il y a 11 jours)
|
||||
À gael@lhopital.org, api
|
||||
|
||||
Bonjour,
|
||||
|
||||
Veuillez trouver ci-dessous la clé API vous permettant d’accéder à nos API Indices de la qualité de l'air, Épisodes, Cartographie, Pollens :
|
||||
|
||||
5a923300-e93d-2a0f-4321-96f115f19100
|
||||
|
||||
Cette clé sera active à partir de demain.
|
||||
|
||||
Nous ouvrons ces API dans une démarche d’amélioration continue, n’hésitez pas à revenir vers nous pour des retours d’expérience et des propositions d’améliorations.
|
||||
|
||||
La documentation des APIs se trouve à l’adresse suivante :
|
||||
https://api.airparif.fr/docs
|
||||
|
||||
Celle concernant les services Web de cartographie se trouve ici :
|
||||
https://www.airparif.fr/doc_api_carto/
|
||||
|
||||
Les informations qui vous concernent sont destinées uniquement à Airparif. Votre adresse e-mail sera utilisée uniquement pour vous informer à propos des API. Vous disposez d'un droit d'accès, de modification, de rectification et de suppression de ces données (art. 34 de la loi "Informatique et Libertés").
|
||||
|
||||
Pour toute question concernant vos données personnelles ou nos API, nous vous invitons à nous contacter via notre formulaire en ligne :
|
||||
https://www.airparif.asso.fr/contact
|
||||
|
||||
Bonne journée !
|
||||
|
||||
Airparif
|
17
bundles/org.openhab.binding.airparif/pom.xml
Executable file
17
bundles/org.openhab.binding.airparif/pom.xml
Executable file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.airparif</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: AirParif Binding</name>
|
||||
|
||||
</project>
|
9
bundles/org.openhab.binding.airparif/src/main/feature/feature.xml
Executable file
9
bundles/org.openhab.binding.airparif/src/main/feature/feature.xml
Executable file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.airparif-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-airparif" description="AirParif Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.airparif/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AirParifBindingConstants} class defines common constants, which are used across the whole binding.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirParifBindingConstants {
|
||||
public static final String BINDING_ID = "airparif";
|
||||
public static final String LOCAL = "local";
|
||||
|
||||
// List of Bridge Type UIDs
|
||||
public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "api");
|
||||
|
||||
// List of Things Type UIDs
|
||||
public static final ThingTypeUID LOCATION_THING_TYPE = new ThingTypeUID(BINDING_ID, "location");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_1 = "channel1";
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE,
|
||||
LOCATION_THING_TYPE);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* An exception that occurred while communicating with Air Parif API server or related processes.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirParifException extends Exception {
|
||||
private static final long serialVersionUID = 4234683995736417341L;
|
||||
|
||||
public AirParifException(String format, Object... args) {
|
||||
super(format.formatted(args));
|
||||
}
|
||||
|
||||
public AirParifException(Exception e, String format, Object... args) {
|
||||
super(format.formatted(args), e);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal;
|
||||
|
||||
import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer;
|
||||
import org.openhab.binding.airparif.internal.handler.AirParifBridgeHandler;
|
||||
import org.openhab.binding.airparif.internal.handler.LocationHandler;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link AirParifHandlerFactory} is responsible for creating things and thing handlers.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.airparif", service = ThingHandlerFactory.class)
|
||||
public class AirParifHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final AirParifDeserializer deserializer;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
@Activate
|
||||
public AirParifHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference AirParifDeserializer deserializer) {
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.deserializer = deserializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
return APIBRIDGE_THING_TYPE.equals(thingTypeUID)
|
||||
? new AirParifBridgeHandler((Bridge) thing, httpClient, deserializer)
|
||||
: LOCATION_THING_TYPE.equals(thingTypeUID) ? new LocationHandler(thing) : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal;
|
||||
|
||||
import static org.openhab.binding.airparif.internal.AirParifBindingConstants.BINDING_ID;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.openhab.core.ui.icon.IconProvider;
|
||||
import org.openhab.core.ui.icon.IconSet;
|
||||
import org.openhab.core.ui.icon.IconSet.Format;
|
||||
import org.osgi.framework.Bundle;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AirParifIconProvider} is the class providing binding related icons.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@Component(service = { IconProvider.class, AirParifIconProvider.class })
|
||||
@NonNullByDefault
|
||||
public class AirParifIconProvider implements IconProvider {
|
||||
private static final String DEFAULT_LABEL = "Air Parif Icons";
|
||||
private static final String DEFAULT_DESCRIPTION = "Icons illustrating air quality levels provided by AirParif";
|
||||
private static final List<String> ICONS = List.of("average", "bad", "degrated", "extremely-bad", "good", "pollen");
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirParifIconProvider.class);
|
||||
private final TranslationProvider i18nProvider;
|
||||
private final Bundle bundle;
|
||||
|
||||
@Activate
|
||||
public AirParifIconProvider(final BundleContext context, final @Reference TranslationProvider i18nProvider) {
|
||||
this.i18nProvider = i18nProvider;
|
||||
this.bundle = context.getBundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IconSet> getIconSets() {
|
||||
return getIconSets(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IconSet> getIconSets(@Nullable Locale locale) {
|
||||
String label = getText("label", DEFAULT_LABEL, locale);
|
||||
String description = getText("decription", DEFAULT_DESCRIPTION, locale);
|
||||
|
||||
return Set.of(new IconSet(BINDING_ID, label, description, Set.of(Format.SVG)));
|
||||
}
|
||||
|
||||
private String getText(String entry, String defaultValue, @Nullable Locale locale) {
|
||||
String text = locale == null ? null : i18nProvider.getText(bundle, "iconset." + entry, defaultValue, locale);
|
||||
return text == null ? defaultValue : text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Integer hasIcon(String category, String iconSetId, Format format) {
|
||||
return Format.SVG.equals(format) && iconSetId.equals(BINDING_ID) && ICONS.contains(category) ? 0 : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) {
|
||||
URL iconResource = bundle.getEntry("icon/%s.svg".formatted(category));
|
||||
|
||||
String result;
|
||||
try (InputStream stream = iconResource.openStream()) {
|
||||
result = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Unable to load ressource '{}': {}", iconResource.getPath(), e.getMessage());
|
||||
result = "";
|
||||
}
|
||||
|
||||
return result.isEmpty() ? null : new ByteArrayInputStream(result.getBytes());
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.api;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* {@link AirParifApi} class defines paths used to interact with server api
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirParifApi {
|
||||
private static final UriBuilder AIRPARIF_BUILDER = UriBuilder.fromPath("/").scheme("https").host("api.airparif.fr");
|
||||
public static final URI VERSION_URI = AIRPARIF_BUILDER.clone().path("version").build();
|
||||
public static final URI KEY_INFO_URI = AIRPARIF_BUILDER.clone().path("key-info").build();
|
||||
public static final URI HORAIR_URI = AIRPARIF_BUILDER.clone().path("horair").path("itineraire").build();
|
||||
public static final URI EPISODES_URI = AIRPARIF_BUILDER.clone().path("episodes").path("en-cours-et-prevus").build();
|
||||
|
||||
private static final UriBuilder INDICES_BUILDER = AIRPARIF_BUILDER.clone().path("indices").path("prevision");
|
||||
public static final URI PREV_COLORS_URI = INDICES_BUILDER.clone().path("couleurs").build();
|
||||
public static final URI PREV_BULLETIN_URI = INDICES_BUILDER.clone().path("bulletin").build();
|
||||
|
||||
private static final UriBuilder POLLENS_BUILDER = AIRPARIF_BUILDER.clone().path("pollens");
|
||||
public static final URI POLLENS_URI = POLLENS_BUILDER.clone().path("bulletin").build();
|
||||
|
||||
// Poor interest, only returns highest risk level for the dept.
|
||||
// public static final UriBuilder POLLENS_DEPT_BUILDER = POLLENS_BUILDER.clone().path("departement");
|
||||
|
||||
public enum Scope {
|
||||
@SerializedName("Cartes et résultats Hor'Air")
|
||||
MAPS,
|
||||
@SerializedName("Pollens")
|
||||
POLLENS,
|
||||
@SerializedName("Épisodes")
|
||||
EVENTS,
|
||||
@SerializedName("Indices")
|
||||
INDEXES,
|
||||
UNKNOWN;
|
||||
}
|
||||
|
||||
public enum Appreciation {
|
||||
GOOD("Bon"),
|
||||
AVERAGE("Moyen"),
|
||||
DEGRATED("Dégradé"),
|
||||
BAD("Mauvais"),
|
||||
REALLY_BAD("Très Mauvais"),
|
||||
EXTREMELY_BAD("Extrêmement Mauvais"),
|
||||
UNKNOWN("");
|
||||
|
||||
public final String apiName;
|
||||
|
||||
Appreciation(String apiName) {
|
||||
this.apiName = apiName;
|
||||
}
|
||||
|
||||
public static final EnumSet<Appreciation> 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<Pollen> AS_SET = EnumSet.allOf(Pollen.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.api;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifApi.Scope;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* {@link AirParifDto} class defines DTO used to interact with server api
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirParifDto {
|
||||
public record Version(//
|
||||
String version) {
|
||||
}
|
||||
|
||||
public record KeyInfo(//
|
||||
ZonedDateTime expiration, //
|
||||
@SerializedName("droits") Set<Scope> scopes) {
|
||||
}
|
||||
|
||||
private record Message(//
|
||||
String fr, //
|
||||
@Nullable String en) {
|
||||
}
|
||||
|
||||
public record PollutantConcentration(//
|
||||
Pollutant pollutant, //
|
||||
int min, //
|
||||
int max) {
|
||||
}
|
||||
|
||||
public record PollutantEpisode(//
|
||||
@SerializedName("nom") Pollutant pollutant, //
|
||||
@SerializedName("niveau") String level) {
|
||||
}
|
||||
|
||||
public record DailyBulletin(//
|
||||
@SerializedName("date") LocalDate previsionDate, //
|
||||
@SerializedName("date_previ") LocalDate productionDate, //
|
||||
@SerializedName("disponible") boolean available, //
|
||||
Message bulletin, //
|
||||
Set<PollutantConcentration> concentrations) {
|
||||
public String dayDescription() {
|
||||
return bulletin.fr;
|
||||
}
|
||||
}
|
||||
|
||||
public record DailyEpisode(//
|
||||
@SerializedName("actif") boolean active, //
|
||||
@SerializedName("polluants") Set<PollutantEpisode> 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<String, PollenAlertLevel[]> valeurs, //
|
||||
String commentaire, //
|
||||
String periode) {
|
||||
|
||||
private static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yy");
|
||||
private static Pattern PATTERN = Pattern.compile("\\d{2}.\\d{2}.\\d{2}");
|
||||
|
||||
private static @Nullable LocalDate getValidity(String periode, boolean begin) {
|
||||
Matcher matcher = PATTERN.matcher(periode);
|
||||
if (matcher.find()) {
|
||||
String extractedDate = matcher.group();
|
||||
if (begin) {
|
||||
return LocalDate.parse(extractedDate, FORMATTER);
|
||||
}
|
||||
if (matcher.find()) {
|
||||
extractedDate = matcher.group();
|
||||
return LocalDate.parse(extractedDate, FORMATTER);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable LocalDate beginValidity() {
|
||||
return getValidity(periode, true);
|
||||
}
|
||||
|
||||
public @Nullable LocalDate endValidity() {
|
||||
return getValidity(periode, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public record PollensResponse(ArrayList<Pollens> data) {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.api;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.openhab.binding.airparif.internal.api.AirParifApi.Appreciation;
|
||||
|
||||
/**
|
||||
* Class association between air quality appreciation and its color
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
public class ColorMap extends HashMap<Appreciation, String> {
|
||||
private static final long serialVersionUID = -605462873565278453L;
|
||||
|
||||
private static Appreciation fromApiName(String searched) {
|
||||
return Objects.requireNonNull(
|
||||
Appreciation.AS_SET.stream().filter(mt -> searched.equals(mt.apiName)).findFirst().orElse(Appreciation.UNKNOWN));
|
||||
}
|
||||
|
||||
public String put(String key, String value) {
|
||||
return super.put(fromApiName(key), value);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.api;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum PollenAlertLevel {
|
||||
@SerializedName("0")
|
||||
NONE(0),
|
||||
@SerializedName("1")
|
||||
LOW(1),
|
||||
@SerializedName("2")
|
||||
AVERAGE(2),
|
||||
@SerializedName("3")
|
||||
HIGH(3),
|
||||
UNKNOWN(-1);
|
||||
|
||||
public static final EnumSet<PollenAlertLevel> AS_SET = EnumSet.allOf(PollenAlertLevel.class);
|
||||
|
||||
public final int riskLevel;
|
||||
|
||||
PollenAlertLevel(int riskLevel) {
|
||||
this.riskLevel = riskLevel;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.api;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link Pollutant} enum lists all pollutants tracked by AirParif
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Pollutant {
|
||||
PM25,
|
||||
PM10,
|
||||
NO2,
|
||||
O3,
|
||||
UNKNOWN;
|
||||
|
||||
public static final EnumSet<Pollutant> AS_SET = EnumSet.allOf(Pollutant.class);
|
||||
|
||||
public static Pollutant safeValueOf(String searched) {
|
||||
try {
|
||||
return Pollutant.valueOf(searched);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Pollutant.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link BridgeConfiguration} is the class used to match the bridge configuration.
|
||||
*
|
||||
* @author Gaël L"hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BridgeConfiguration {
|
||||
public String apikey = "";
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link LocationConfiguration} is the class used to match the
|
||||
* thing configuration.
|
||||
*
|
||||
* @author Gaël L"hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LocationConfiguration {
|
||||
public static final String LOCATION = "location";
|
||||
|
||||
public int refresh = 10;
|
||||
public String location = "";
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.db;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonIOException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link DepartmentDbService} makes available a list of known French Metropolitan departments.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial Contribution
|
||||
*/
|
||||
|
||||
@Component(service = DepartmentDbService.class)
|
||||
@NonNullByDefault
|
||||
public class DepartmentDbService {
|
||||
private final Logger logger = LoggerFactory.getLogger(DepartmentDbService.class);
|
||||
private final List<Department> departments = new ArrayList<>();
|
||||
|
||||
public record Department(String id, String name, double northestLat, double southestLat, double eastestLon,
|
||||
double westestLon) {
|
||||
}
|
||||
|
||||
@Activate
|
||||
public DepartmentDbService() {
|
||||
try (InputStream is = Thread.currentThread().getContextClassLoader()
|
||||
.getResourceAsStream("/db/departments.json");
|
||||
Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
|
||||
departments.addAll(Arrays.asList(gson.fromJson(reader, Department[].class)));
|
||||
logger.debug("Successfully loaded {} departments", departments.size());
|
||||
} catch (IOException | JsonSyntaxException | JsonIOException e) {
|
||||
logger.warn("Unable to load departments list: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public List<Department> 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.deserialization;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airparif.internal.AirParifException;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.PollutantConcentration;
|
||||
import org.openhab.binding.airparif.internal.api.ColorMap;
|
||||
import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link AirParifDeserializer} is responsible to instantiate suitable Gson (de)serializer
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = AirParifDeserializer.class)
|
||||
public class AirParifDeserializer {
|
||||
private final Gson gson;
|
||||
|
||||
@Activate
|
||||
public AirParifDeserializer(final @Reference TimeZoneProvider timeZoneProvider) {
|
||||
gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.registerTypeAdapter(PollenAlertLevel.class, new PollenAlertLevelDeserializer())
|
||||
.registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory())
|
||||
.registerTypeAdapter(ColorMap.class, new ColorMapDeserializer())
|
||||
.registerTypeAdapter(PollutantConcentration.class, new PollutantConcentrationDeserializer())
|
||||
.registerTypeAdapter(LocalDate.class,
|
||||
(JsonDeserializer<LocalDate>) (json, type, context) -> LocalDate
|
||||
.parse(json.getAsJsonPrimitive().getAsString()))
|
||||
.registerTypeAdapter(ZonedDateTime.class,
|
||||
(JsonDeserializer<ZonedDateTime>) (json, type, context) -> ZonedDateTime
|
||||
.parse(json.getAsJsonPrimitive().getAsString() + "Z")
|
||||
.withZoneSameInstant(timeZoneProvider.getTimeZone()))
|
||||
.create();
|
||||
}
|
||||
|
||||
public <T> T deserialize(Class<T> clazz, String json) throws AirParifException {
|
||||
try {
|
||||
@Nullable
|
||||
T result = gson.fromJson(json, clazz);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
throw new AirParifException("Deserialization of '%s' resulted in null value", json);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new AirParifException(e, "Unexpected error deserializing '%s'", json);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.deserialization;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airparif.internal.api.ColorMap;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
/**
|
||||
* Specialized deserializer for ColorMap class
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class ColorMapDeserializer implements JsonDeserializer<ColorMap> {
|
||||
|
||||
@Override
|
||||
public @Nullable ColorMap deserialize(JsonElement json, Type clazz, JsonDeserializationContext context) {
|
||||
ColorMap result = new ColorMap();
|
||||
Set<Map.Entry<String, JsonElement>> entrySet = json.getAsJsonObject().entrySet();
|
||||
entrySet.forEach(entry -> result.put(entry.getKey(), entry.getValue().getAsString()));
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.deserialization;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Specialized deserializer for ColorMap class
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class PollenAlertLevelDeserializer implements JsonDeserializer<PollenAlertLevel> {
|
||||
|
||||
@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);
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.deserialization;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.PollutantConcentration;
|
||||
import org.openhab.binding.airparif.internal.api.Pollutant;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Specialized deserializer for ColorMap class
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class PollutantConcentrationDeserializer implements JsonDeserializer<PollutantConcentration> {
|
||||
|
||||
@Override
|
||||
public @Nullable PollutantConcentration deserialize(JsonElement json, Type clazz,
|
||||
JsonDeserializationContext context) {
|
||||
PollutantConcentration result = null;
|
||||
JsonArray array = json.getAsJsonArray();
|
||||
|
||||
if (array.size() == 3) {
|
||||
Pollutant pollutant = Pollutant.safeValueOf(array.get(0).getAsString());
|
||||
try {
|
||||
result = new PollutantConcentration(pollutant, array.get(1).getAsInt(), array.get(2).getAsInt());
|
||||
} catch (JsonSyntaxException ignore) {
|
||||
// result will remain null
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.deserialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
/**
|
||||
* Enforces a fallback to UNKNOWN when deserializing enum types, marked as @NonNull whereas they were valued
|
||||
* to null if the appropriate value is absent.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class StrictEnumTypeAdapterFactory implements TypeAdapterFactory {
|
||||
|
||||
@Override
|
||||
public @Nullable <T> TypeAdapter<T> create(@NonNullByDefault({}) Gson gson,
|
||||
@NonNullByDefault({}) TypeToken<T> type) {
|
||||
return type.getRawType().isEnum() ? newStrictEnumAdapter(gson.getDelegateAdapter(this, type)) : null;
|
||||
}
|
||||
|
||||
private <T> TypeAdapter<T> newStrictEnumAdapter(@NonNullByDefault({}) TypeAdapter<T> 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*;
|
||||
import static org.openhab.binding.airparif.internal.config.LocationConfiguration.LOCATION;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.airparif.internal.handler.AirParifBridgeHandler;
|
||||
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.LocationProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AirParifDiscoveryService} creates things based on the configured location.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial Contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = AirParifDiscoveryService.class)
|
||||
@NonNullByDefault
|
||||
public class AirParifDiscoveryService extends AbstractThingHandlerDiscoveryService<AirParifBridgeHandler> {
|
||||
private static final int DISCOVER_TIMEOUT_SECONDS = 2;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirParifDiscoveryService.class);
|
||||
|
||||
private @NonNullByDefault({}) LocationProvider locationProvider;
|
||||
|
||||
public AirParifDiscoveryService() {
|
||||
super(AirParifBridgeHandler.class, SUPPORTED_THING_TYPES_UIDS, DISCOVER_TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
@Reference(unbind = "-")
|
||||
public void bindTranslationProvider(TranslationProvider translationProvider) {
|
||||
this.i18nProvider = translationProvider;
|
||||
}
|
||||
|
||||
@Reference(unbind = "-")
|
||||
public void bindLocaleProvider(LocaleProvider localeProvider) {
|
||||
this.localeProvider = localeProvider;
|
||||
}
|
||||
|
||||
@Reference(unbind = "-")
|
||||
public void bindLocationProvider(LocationProvider locationProvider) {
|
||||
this.locationProvider = locationProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting AirParif discovery scan");
|
||||
if (locationProvider.getLocation() instanceof PointType location) {
|
||||
ThingUID bridgeUID = thingHandler.getThing().getUID();
|
||||
thingDiscovered(DiscoveryResultBuilder.create(new ThingUID(LOCATION_THING_TYPE, bridgeUID, LOCAL))
|
||||
.withLabel("@text/discovery.airparif.location.local.label") //
|
||||
.withProperty(LOCATION, location.toString()) //
|
||||
.withRepresentationProperty(LOCATION) //
|
||||
.withBridge(bridgeUID).build());
|
||||
} else {
|
||||
logger.debug("LocationProvider.getLocation() is not set, no discovery results can be provided");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airparif.internal.api.AirParifApi.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpStatus.Code;
|
||||
import org.openhab.binding.airparif.internal.AirParifException;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.Bulletin;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.Episode;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.KeyInfo;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.Pollens;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.PollensResponse;
|
||||
import org.openhab.binding.airparif.internal.api.AirParifDto.Version;
|
||||
import org.openhab.binding.airparif.internal.api.ColorMap;
|
||||
import org.openhab.binding.airparif.internal.config.BridgeConfiguration;
|
||||
import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link AirParifBridgeHandler} is the handler for OpenUV API and connects it
|
||||
* to the webservice.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirParifBridgeHandler extends BaseBridgeHandler {
|
||||
private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirParifBridgeHandler.class);
|
||||
private final AirParifDeserializer deserializer;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
private BridgeConfiguration config = new BridgeConfiguration();
|
||||
|
||||
public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer) {
|
||||
super(bridge);
|
||||
this.deserializer = deserializer;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing AirParif bridge handler.");
|
||||
config = getConfigAs(BridgeConfiguration.class);
|
||||
if (config.apikey.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.config-error-unknown-apikey");
|
||||
return;
|
||||
}
|
||||
initiateConnexion();
|
||||
}
|
||||
|
||||
public synchronized String executeUri(URI uri) throws AirParifException {
|
||||
logger.debug("executeUrl: {} ", uri);
|
||||
|
||||
Request request = httpClient.newRequest(uri).method(HttpMethod.GET)
|
||||
.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.header(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON).header("X-Api-Key", config.apikey);
|
||||
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
|
||||
Code statusCode = HttpStatus.getCode(response.getStatus());
|
||||
|
||||
if (statusCode == Code.OK) {
|
||||
String content = new String(response.getContent(), DEFAULT_CHARSET);
|
||||
logger.trace("executeUrl: {} returned {}", uri, content);
|
||||
return content;
|
||||
} else if (statusCode == Code.FORBIDDEN) {
|
||||
throw new AirParifException("@text/offline.config-error-invalid-apikey");
|
||||
}
|
||||
|
||||
throw new AirParifException("Error '%s' requesting: %s", statusCode.getMessage(), uri.toString());
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new AirParifException(e, "Exception while calling %s", request.getURI());
|
||||
} catch (InterruptedException e) {
|
||||
throw new AirParifException(e, "Execution interrupted: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized <T> T executeUri(URI uri, Class<T> clazz) throws AirParifException {
|
||||
String content = executeUri(uri);
|
||||
return deserializer.deserialize(clazz, content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("The AirParif bridge does not handles commands");
|
||||
}
|
||||
|
||||
private void initiateConnexion() {
|
||||
Version version;
|
||||
KeyInfo keyInfo;
|
||||
|
||||
try { // This does validate communication with the server
|
||||
version = executeUri(VERSION_URI, Version.class);
|
||||
} catch (AirParifException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try { // This validates the api key value
|
||||
keyInfo = executeUri(KEY_INFO_URI, KeyInfo.class);
|
||||
} catch (AirParifException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
getThing().setProperty("api-version", version.version());
|
||||
getThing().setProperty("key-expiration", keyInfo.expiration().toString());
|
||||
logger.info("The api key is valid until {}", keyInfo.expiration().toString());
|
||||
getThing().setProperty("scopes", keyInfo.scopes().stream().map(e -> e.name()).collect(Collectors.joining(",")));
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
try {
|
||||
ColorMap map = executeUri(PREV_COLORS_URI, ColorMap.class);
|
||||
logger.info("The color map is {}", map.toString());
|
||||
|
||||
Bulletin bulletin = executeUri(PREV_BULLETIN_URI, Bulletin.class);
|
||||
logger.info("The bulletin is {}", bulletin.today().dayDescription());
|
||||
|
||||
Episode episode = executeUri(EPISODES_URI, Episode.class);
|
||||
logger.info("The bulletin is {}", episode);
|
||||
|
||||
Pollens pollens = executeUri(POLLENS_URI, PollensResponse.class).data().get(0);
|
||||
logger.info("The pollens are {}", pollens);
|
||||
LocalDate begin = pollens.beginValidity();
|
||||
LocalDate end = pollens.endValidity();
|
||||
|
||||
String response = executeUri(POLLENS_DEPT_BUILDER.path("78").build());
|
||||
logger.info("The pollens 78 {}", response);
|
||||
} catch (AirParifException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.airparif.internal.handler;
|
||||
|
||||
import static org.openhab.binding.airparif.internal.AirParifBindingConstants.CHANNEL_1;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.airparif.internal.config.LocationConfiguration;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link LocationHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LocationHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LocationHandler.class);
|
||||
|
||||
private @Nullable LocationConfiguration config;
|
||||
|
||||
public LocationHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (CHANNEL_1.equals(channelUID.getId())) {
|
||||
if (command instanceof RefreshType) {
|
||||
// TODO: handle data refresh
|
||||
}
|
||||
|
||||
// TODO: handle command
|
||||
|
||||
// Note: if communication with thing fails for some reason,
|
||||
// indicate that by setting the status with detail information:
|
||||
// updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
// "Could not control device at IP address x.x.x.x");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(LocationConfiguration.class);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
// Example for background initialization:
|
||||
scheduler.execute(() -> {
|
||||
boolean thingReachable = true; // <background task with long running initialization here>
|
||||
// when done do:
|
||||
if (thingReachable) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="airparif" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>AirParif Binding</name>
|
||||
<description>Air Quality data and forecasts provided by AirParif.</description>
|
||||
<connection>cloud</connection>
|
||||
<countries>fr</countries>
|
||||
|
||||
</addon:addon>
|
@ -0,0 +1,14 @@
|
||||
|
||||
# discovery result
|
||||
|
||||
discovery.airparif.location.local.label = Air Quality Report
|
||||
|
||||
# iconprovider
|
||||
|
||||
iconset.label = AirParif Icons
|
||||
iconset.description = Icons illustrating air quality measures provided by AirParif
|
||||
|
||||
# thing status descriptions
|
||||
|
||||
offline.config-error-unknown-apikey = Parameter 'apikey' must be configured
|
||||
offline.config-error-invalid-apikey = Parameter 'apikey' is invalid
|
22
bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml
Executable file
22
bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml
Executable file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="airparif"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="api">
|
||||
<label>AirParif API Portal</label>
|
||||
<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.
|
||||
</description>
|
||||
<config-description>
|
||||
<parameter name="apikey" type="text" required="true">
|
||||
<label>API Key</label>
|
||||
<description>Token used to access the service</description>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="airparif"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<channel-type id="alert-level">
|
||||
<item-type>Number</item-type>
|
||||
<label>@text/alertLevelChannelLabel</label>
|
||||
<description>@text/alertLevelChannelDescription</description>
|
||||
<category>error</category>
|
||||
<tags>
|
||||
<tag>Alarm</tag>
|
||||
</tags>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">Good</option>
|
||||
<option value="1">Average</option>
|
||||
<option value="2">Degrated</option>
|
||||
<option value="3">Bad</option>
|
||||
<option value="4">Extremely Bad</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="timestamp" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>@text/timestampChannelLabel</label>
|
||||
<description>@text/timestampChannelDescription</description>
|
||||
<category>time</category>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dominent">
|
||||
<item-type>String</item-type>
|
||||
<label>@text/dominentChannelLabel</label>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="pm25">@text/pollutantPm25</option>
|
||||
<option value="pm10">@text/pollutantPm10</option>
|
||||
<option value="o3">@text/pollutantO3</option>
|
||||
<option value="no2">@text/pollutantNO2</option>
|
||||
<option value="co">@text/pollutantCO</option>
|
||||
<option value="so2">@text/pollutantSO2</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rain-intensity">
|
||||
<item-type>Number</item-type>
|
||||
<label>Intensity</label>
|
||||
<description>Rain intensity level</description>
|
||||
<category>oh:meteofrance:intensity</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">Dry Weather</option>
|
||||
<option value="1">Light Rain</option>
|
||||
<option value="2">Moderate Rain</option>
|
||||
<option value="3">Heavy Rain</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="vent">
|
||||
<item-type>Number</item-type>
|
||||
<label>Wind</label>
|
||||
<description>Wind event alert level</description>
|
||||
<category>oh:meteofrance:vent</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="orage">
|
||||
<item-type>Number</item-type>
|
||||
<label>Storm</label>
|
||||
<description>Storm alert level</description>
|
||||
<category>oh:meteofrance:orage</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="inondation">
|
||||
<item-type>Number</item-type>
|
||||
<label>Flood</label>
|
||||
<description>Flood alert level</description>
|
||||
<category>oh:meteofrance:inondation</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="neige">
|
||||
<item-type>Number</item-type>
|
||||
<label>Snow</label>
|
||||
<description>Snow event alert level</description>
|
||||
<category>oh:meteofrance:neige</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="canicule">
|
||||
<item-type>Number</item-type>
|
||||
<label>Heat Wave</label>
|
||||
<description>High temperature alert level</description>
|
||||
<category>oh:meteofrance:canicule</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="grand-froid">
|
||||
<item-type>Number</item-type>
|
||||
<label>Extreme Cold</label>
|
||||
<description>Negative temperature alert level</description>
|
||||
<category>oh:meteofrance:grand-froid</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="avalanches">
|
||||
<item-type>Number</item-type>
|
||||
<label>Avalanches</label>
|
||||
<description>Avalanche alert level</description>
|
||||
<category>oh:meteofrance:avalanches</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="vague-submersion">
|
||||
<item-type>Number</item-type>
|
||||
<label>Wave Submersion</label>
|
||||
<description>Submersion wave alert level</description>
|
||||
<category>oh:meteofrance:vague-submersion</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pluie-inondation">
|
||||
<item-type>Number</item-type>
|
||||
<label>Rain Flood</label>
|
||||
<description>Flood caused by rainfall alert level</description>
|
||||
<category>oh:meteofrance:pluie-inondation</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">No special vigilance</option>
|
||||
<option value="1">Be attentive</option>
|
||||
<option value="2">Be very vigilant</option>
|
||||
<option value="3">Absolute vigilance</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="comment">
|
||||
<item-type>String</item-type>
|
||||
<label>Comment</label>
|
||||
<category>text</category>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="timestamp" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Observation Timestamp</label>
|
||||
<category>time</category>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="condition-icon">
|
||||
<item-type>Image</item-type>
|
||||
<label>Icon</label>
|
||||
<description>Pictogram associated with the alert level.</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="airparif"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="location">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="api"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Air Quality Report</label>
|
||||
<description>AirParif air quality report for the given location</description>
|
||||
|
||||
<channels>
|
||||
<channel id="channel1" typeId="sample-channel"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="location" type="text" required="false"
|
||||
pattern="^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)[,]\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$">
|
||||
<context>location</context>
|
||||
<label>Location</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
</thing:thing-descriptions>
|
@ -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
|
||||
}
|
||||
]
|
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
|
||||
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#65bba2;stroke-miterlimit:10;stroke-width:14px"/>
|
||||
<circle cx="59.23" cy="41.74" r="3.57" class="cls-2"/>
|
||||
<circle cx="36.76" cy="41.74" r="3.57" class="cls-2"/>
|
||||
<path d="m35.27 59.3 26.81-.05"
|
||||
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px"/>
|
||||
</svg>
|
After Width: | Height: | Size: 452 B |
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
|
||||
<circle cx="47.85" cy="48.04" r="34.72" style="stroke-miterlimit:10;fill:#fff;stroke:#ea5553;stroke-width:14px"/>
|
||||
<path d="M34.46 63.64s4.34-8.68 13.28-8.68 12.34 4.86 14.81 8.43"
|
||||
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-width:3px;stroke-linejoin:round"/>
|
||||
<circle cx="59.48" cy="41.74" r="3.57" class="cls-3"/>
|
||||
<circle cx="37.01" cy="41.74" r="3.57" class="cls-3"/>
|
||||
<path d="m32.51 30.33 16.13 7.93 15.59-7.93"
|
||||
style="stroke-miterlimit:10;fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-width:3px"/>
|
||||
</svg>
|
After Width: | Height: | Size: 635 B |
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
|
||||
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#ffe00a;stroke-miterlimit:10;stroke-width:14px"/>
|
||||
<path d="M34.29 63.64s4.34-8.68 13.28-8.68 12.34 4.86 14.81 8.43"
|
||||
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px"/>
|
||||
<circle cx="59.32" cy="41.74" r="3.57" class="cls-2"/>
|
||||
<circle cx="36.85" cy="41.74" r="3.57" class="cls-2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 486 B |
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
|
||||
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#7b2681;stroke-miterlimit:10;stroke-width:14px"/>
|
||||
<path
|
||||
d="m63.81 60.51-4.15-4.06-4.15 3.99-4.07-4.23-3.83 4.23h-.01l-4.15-4.07-4.15 3.99-4.07-4.23-3.83 4.23M35.36 32.72l8.17 5.62-8.17 6.64M60.11 32.72l-8.17 5.62 8.17 6.64"
|
||||
class="cls-1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 401 B |
@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
|
||||
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#8bd1f5;stroke-miterlimit:10;stroke-width:14px"/>
|
||||
<path
|
||||
d="M47.45 65.74c-4.39 0-9.68-2.37-12.06-6.57-2.18-3.85 2.56-3.66 2.56-3.66h20.84s4.81-.25 2.56 3.66c-2.41 4.19-7.68 6.57-12.06 6.57h-1.83Z"
|
||||
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px"/>
|
||||
<circle cx="58.74" cy="41.74" r="3.57" class="cls-3"/>
|
||||
<circle cx="36.27" cy="41.74" r="3.57" class="cls-3"/>
|
||||
</svg>
|
After Width: | Height: | Size: 566 B |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
Loading…
Reference in New Issue
Block a user