Home Assistant Git Exporter
This commit is contained in:
3
config/custom_components/pollens/README.md
Normal file
3
config/custom_components/pollens/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Pollens-Async
|
||||
|
||||
Custom component for pollens
|
||||
191
config/custom_components/pollens/__init__.py
Normal file
191
config/custom_components/pollens/__init__.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""Pollens Allergy component."""
|
||||
from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from os import error
|
||||
from re import I
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SENSORS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
CoordinatorEntity,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CONF_LITERAL,
|
||||
CONF_POLLENSLIST,
|
||||
CONF_VERSION,
|
||||
DOMAIN,
|
||||
COORDINATOR,
|
||||
UNDO_LISTENER,
|
||||
CONF_COUNTRYCODE,
|
||||
CONF_SCAN_INTERVAL,
|
||||
KEY_TO_ATTR,
|
||||
)
|
||||
|
||||
from .pollensasync import PollensClient
|
||||
|
||||
# List of platforms to support. There should be a matching .py file for each,
|
||||
# eg <cover.py> and <sensor.py>
|
||||
PLATFORMS: list[str] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up pollens integation"""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up pollens from a config entry."""
|
||||
|
||||
conf = entry.data
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
api = PollensClient(session)
|
||||
|
||||
county = conf[CONF_COUNTRYCODE]
|
||||
scan_interval = entry.options.get(CONF_SCAN_INTERVAL, 3)
|
||||
|
||||
await api.Get(county)
|
||||
|
||||
name = f"Pollens {api.county_name}"
|
||||
|
||||
coordinator = PollensUpdateCoordinator(
|
||||
hass=hass,
|
||||
name=name,
|
||||
scan_interval=scan_interval,
|
||||
county=county,
|
||||
api=api,
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Add and update listener
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
||||
# Setup coordinator
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
COORDINATOR: coordinator,
|
||||
UNDO_LISTENER: undo_listener,
|
||||
"pollens_api": api,
|
||||
}
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
_LOGGER.debug("Setup of %s successful", entry.title)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate the config entry upon new versions."""
|
||||
version = entry.version
|
||||
data = {**entry.data}
|
||||
|
||||
_LOGGER.debug("Migrating from version %s", version)
|
||||
|
||||
# 1 -> 2: Remove unused condition data:
|
||||
if version == 1:
|
||||
data.pop(CONF_SENSORS, None)
|
||||
data[CONF_POLLENSLIST] = [pollen for pollen in KEY_TO_ATTR]
|
||||
data[CONF_LITERAL] = True
|
||||
version = entry.version = CONF_VERSION
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
_LOGGER.debug("Migration to version %s successful", version)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Update when config_entry options update"""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
class PollensUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching Pollens data API"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
scan_interval: int,
|
||||
api: str,
|
||||
county: str,
|
||||
# level_filter: int,
|
||||
) -> None:
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
name=name,
|
||||
update_interval=timedelta(hours=scan_interval),
|
||||
)
|
||||
|
||||
self.api = api
|
||||
self.name = name
|
||||
self.county = county
|
||||
|
||||
async def _async_update_data(self):
|
||||
_LOGGER.info("Update data from web site for %s", self.name)
|
||||
try:
|
||||
return await self.api.Get(self.county)
|
||||
except ClientError as error:
|
||||
raise UpdateFailed(f"Error updating from RSSA : {error}") from error
|
||||
|
||||
|
||||
class PollensEntity(CoordinatorEntity):
|
||||
"""Implementation of the base pollens Entity"""
|
||||
|
||||
_attr_extra_state_attributes = {"attribution": ATTRIBUTION}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PollensUpdateCoordinator,
|
||||
name: str,
|
||||
icon: str,
|
||||
entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize"""
|
||||
|
||||
super().__init__(coordinator=coordinator)
|
||||
|
||||
# self._attr_unique_id = f"{entry.entry_id}_{KEY_TO_ATTR[name.lower()][0]}"
|
||||
# self._attr_icon = icon
|
||||
# self._unique_id = f"pollens_{entry.entry_id}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
str(
|
||||
f"{self.platform.config_entry.unique_id}{self.platform.config_entry.data['county']}"
|
||||
),
|
||||
)
|
||||
},
|
||||
manufacturer="RNSA",
|
||||
model="Pollens sensor",
|
||||
name=self.coordinator.name,
|
||||
)
|
||||
175
config/custom_components/pollens/config_flow.py
Normal file
175
config/custom_components/pollens/config_flow.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Config flow for Pollens integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .const import (
|
||||
CONF_VERSION,
|
||||
DOMAIN,
|
||||
KEY_TO_ATTR,
|
||||
CONF_COUNTRYCODE,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_POLLENSLIST,
|
||||
CONF_LITERAL,
|
||||
)
|
||||
from .dept import DEPARTMENTS
|
||||
|
||||
from .pollensasync import PollensClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
# Validate the data can be used to set up a connection.
|
||||
if len(data[CONF_COUNTRYCODE]) != 2:
|
||||
raise InvalidCounty
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
client = PollensClient(session)
|
||||
result = await client.Get(data[CONF_COUNTRYCODE])
|
||||
if not result:
|
||||
# If there is an error, raise an exception to notify HA that there was a
|
||||
# problem. The UI will also show there was a problem
|
||||
raise CannotConnect
|
||||
title = f"Pollens {client.county_name}"
|
||||
return {"title": title}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Pollens."""
|
||||
|
||||
VERSION = CONF_VERSION
|
||||
# Pick one of the available connection classes in homeassistant/config_entries.py
|
||||
# This tells HA if it should be asking for updates, or it'll be notified of updates
|
||||
# automatically. This example uses PUSH, as the dummy hub will notify HA of
|
||||
# changes.
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize"""
|
||||
self.data = None
|
||||
self._init_info = {}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
self._init_info["data"] = user_input
|
||||
self._init_info["info"] = await validate_input(self.hass, user_input)
|
||||
return await self.async_step_select_pollens()
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidCounty:
|
||||
errors["base"] = "invalid_county"
|
||||
_LOGGER.exception("Invalid county selected")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
entries = self._async_current_entries()
|
||||
# Remove county from the list (already configured)
|
||||
for entry in entries:
|
||||
if entry.data[CONF_COUNTRYCODE] in DEPARTMENTS:
|
||||
DEPARTMENTS.pop(entry.data[CONF_COUNTRYCODE])
|
||||
|
||||
# If there is no user input or there were errors, show the form again, including any errors that were found with the input.
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_COUNTRYCODE, default=["60"]): vol.In(DEPARTMENTS),
|
||||
vol.Required(CONF_LITERAL, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
description_placeholders={"docs_url": "pollens.fr"},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_select_pollens(self, user_input=None):
|
||||
"""Select pollens step 2"""
|
||||
if user_input is not None:
|
||||
_LOGGER.info("Select pollens step")
|
||||
self._init_info["data"][CONF_POLLENSLIST] = user_input[CONF_POLLENSLIST]
|
||||
return self.async_create_entry(
|
||||
title=self._init_info["info"]["title"], data=self._init_info["data"]
|
||||
)
|
||||
pollens = [pollen for pollen in KEY_TO_ATTR]
|
||||
return self.async_show_form(
|
||||
step_id="select_pollens",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Optional(CONF_POLLENSLIST): cv.multi_select(pollens)}
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Options callback for Pollens."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidCounty(exceptions.HomeAssistantError):
|
||||
"""Error to invalid county."""
|
||||
|
||||
|
||||
class InvalidScanInterval(exceptions.HomeAssistantError):
|
||||
"""Error to invalid scan interval ."""
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Config flow options for Pollens."""
|
||||
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize Pollens options flow."""
|
||||
self.config_entry = entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
_LOGGER.info("Changing options of pollens integration")
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
# Validate the data can be used to set up a connection.
|
||||
_LOGGER.info(
|
||||
"Change option of %s to %s",
|
||||
self.config_entry.title,
|
||||
user_input[CONF_SCAN_INTERVAL],
|
||||
)
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
# Configuration of scan interval mini 3 h max 24h
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL,
|
||||
default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 1),
|
||||
): selector.NumberSelector(selector.NumberSelectorConfig(min=3, max=24, mode=selector.NumberSelectorMode.BOX)),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
67
config/custom_components/pollens/const.py
Normal file
67
config/custom_components/pollens/const.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Constants for the Pollens integration."""
|
||||
|
||||
DOMAIN = "pollens"
|
||||
ATTRIBUTION = "Data from Reseau National de Surveillance Aerobiologique "
|
||||
CONF_VERSION = 2
|
||||
COORDINATOR = "coordinator"
|
||||
UNDO_LISTENER = "undo_listener"
|
||||
|
||||
CONF_COUNTRYCODE = "county"
|
||||
CONF_SCAN_INTERVAL = "scan_interval"
|
||||
CONF_SCANINTERVAL = "scaninterval"
|
||||
CONF_POLLENSLIST = "pollens_list"
|
||||
CONF_LITERAL = "literal_states"
|
||||
|
||||
ATTR_TILLEUL = "tilleul"
|
||||
ATTR_AMBROISIES = "ambroisies"
|
||||
ATTR_OLIVIER = "olivier"
|
||||
ATTR_PLANTAIN = "plantain"
|
||||
ATTR_NOISETIER = "noisetier"
|
||||
ATTR_AULNE = "aulne"
|
||||
ATTR_ARMOISE = "armoise"
|
||||
ATTR_CHATAIGNIER = "chataignier"
|
||||
ATTR_URTICACEES = "urticacees"
|
||||
ATTR_OSEILLE = "oseille"
|
||||
ATTR_GRAMINEES = "graminees"
|
||||
ATTR_CHENE = "chene"
|
||||
ATTR_PLATANE = "platane"
|
||||
ATTR_BOULEAU = "bouleau"
|
||||
ATTR_CHARME = "charme"
|
||||
ATTR_PEUPLIER = "peuplier"
|
||||
ATTR_FRENE = "frene"
|
||||
ATTR_SAULE = "saule"
|
||||
ATTR_CYPRES = "cypres"
|
||||
ATTR_CUPRESSASEES = "cupressacees"
|
||||
ATTR_LITERAL_STATE = "literal_state"
|
||||
ATTR_POLLEN_NAME = "pollen_name"
|
||||
|
||||
ICON_FLOWER = "mdi:flower"
|
||||
ICON_TREE = "mdi:tree"
|
||||
ICON_GRASS = "mdi:grass"
|
||||
KEY_TO_ATTR = {
|
||||
"tilleul": [ATTR_TILLEUL, ICON_TREE],
|
||||
"ambroisies": [ATTR_AMBROISIES, ICON_GRASS],
|
||||
"olivier": [ATTR_OLIVIER, ICON_TREE],
|
||||
"plantain": [ATTR_PLANTAIN, ICON_GRASS],
|
||||
"noisetier": [ATTR_NOISETIER, ICON_TREE],
|
||||
"aulne": [ATTR_AULNE, ICON_TREE],
|
||||
"armoise": [ATTR_ARMOISE, ICON_GRASS],
|
||||
"châtaignier": [ATTR_CHATAIGNIER, ICON_TREE],
|
||||
"urticacées": [ATTR_URTICACEES, ICON_GRASS],
|
||||
"oseille": [ATTR_OSEILLE, ICON_GRASS],
|
||||
"graminées": [ATTR_GRAMINEES, ICON_GRASS],
|
||||
"chêne": [ATTR_CHENE, ICON_TREE],
|
||||
"platane": [ATTR_PLATANE, ICON_TREE],
|
||||
"bouleau": [ATTR_BOULEAU, ICON_TREE],
|
||||
"charme": [ATTR_CHARME, ICON_TREE],
|
||||
"peuplier": [ATTR_PEUPLIER, ICON_TREE],
|
||||
"frêne": [ATTR_FRENE, ICON_TREE],
|
||||
"saule": [ATTR_SAULE, ICON_TREE],
|
||||
"cyprès": [ATTR_CYPRES, ICON_TREE],
|
||||
"cupressacées": [ATTR_CUPRESSASEES, ICON_GRASS],
|
||||
}
|
||||
|
||||
ATTR_COUNTY_NAME = "departement"
|
||||
ATTR_URL = "url"
|
||||
|
||||
LIST_RISK = ["nul", "faible", "moyen", "élevé"]
|
||||
105
config/custom_components/pollens/dept.py
Normal file
105
config/custom_components/pollens/dept.py
Normal file
@@ -0,0 +1,105 @@
|
||||
""" Dictionnaire des départements Français """
|
||||
|
||||
DEPARTMENTS = {
|
||||
"01": "Ain",
|
||||
"02": "Aisne",
|
||||
"03": "Allier",
|
||||
"04": "Alpes-de-Haute-Provence",
|
||||
"05": "Hautes-Alpes",
|
||||
"06": "Alpes-Maritimes",
|
||||
"07": "Ardèche",
|
||||
"08": "Ardennes",
|
||||
"09": "Ariège",
|
||||
"10": "Aube",
|
||||
"11": "Aude",
|
||||
"12": "Aveyron",
|
||||
"13": "Bouches-du-Rhône",
|
||||
"14": "Calvados",
|
||||
"15": "Cantal",
|
||||
"16": "Charente",
|
||||
"17": "Charente-Maritime",
|
||||
"18": "Cher",
|
||||
"19": "Corrèze",
|
||||
"20": "Corse-du-Sud",
|
||||
"20": "Haute-Corse",
|
||||
"21": "Côte-d'Or",
|
||||
"22": "Côtes-d'Armor",
|
||||
"23": "Creuse",
|
||||
"24": "Dordogne",
|
||||
"25": "Doubs",
|
||||
"26": "Drôme",
|
||||
"27": "Eure",
|
||||
"28": "Eure-et-Loir",
|
||||
"29": "Finistère",
|
||||
"30": "Gard",
|
||||
"31": "Haute-Garonne",
|
||||
"32": "Gers",
|
||||
"33": "Gironde",
|
||||
"34": "Hérault",
|
||||
"35": "Ille-et-Vilaine",
|
||||
"36": "Indre",
|
||||
"37": "Indre-et-Loire",
|
||||
"38": "Isère",
|
||||
"39": "Jura",
|
||||
"40": "Landes",
|
||||
"41": "Loir-et-Cher",
|
||||
"42": "Loire",
|
||||
"43": "Haute-Loire",
|
||||
"44": "Loire-Atlantique",
|
||||
"45": "Loiret",
|
||||
"46": "Lot",
|
||||
"47": "Lot-et-Garonne",
|
||||
"48": "Lozère",
|
||||
"49": "Maine-et-Loire",
|
||||
"50": "Manche",
|
||||
"51": "Marne",
|
||||
"52": "Haute-Marne",
|
||||
"53": "Mayenne",
|
||||
"54": "Meurthe-et-Moselle",
|
||||
"55": "Meuse",
|
||||
"56": "Morbihan",
|
||||
"57": "Moselle",
|
||||
"58": "Nièvre",
|
||||
"59": "Nord",
|
||||
"60": "Oise",
|
||||
"61": "Orne",
|
||||
"62": "Pas-de-Calais",
|
||||
"63": "Puy-de-Dôme",
|
||||
"64": "Pyrénées-Atlantiques",
|
||||
"65": "Hautes-Pyrénées",
|
||||
"66": "Pyrénées-Orientales",
|
||||
"67": "Bas-Rhin",
|
||||
"68": "Haut-Rhin",
|
||||
"69": "Rhône",
|
||||
"70": "Haute-Saône",
|
||||
"71": "Saône-et-Loire",
|
||||
"72": "Sarthe",
|
||||
"73": "Savoie",
|
||||
"74": "Haute-Savoie",
|
||||
"75": "Paris",
|
||||
"76": "Seine-Maritime",
|
||||
"77": "Seine-et-Marne",
|
||||
"78": "Yvelines",
|
||||
"79": "Deux-Sèvres",
|
||||
"80": "Somme",
|
||||
"81": "Tarn",
|
||||
"82": "Tarn-et-Garonne",
|
||||
"83": "Var",
|
||||
"84": "Vaucluse",
|
||||
"85": "Vendée",
|
||||
"86": "Vienne",
|
||||
"87": "Haute-Vienne",
|
||||
"88": "Vosges",
|
||||
"89": "Yonne",
|
||||
"90": "Territoire de Belfort",
|
||||
"91": "Essonne",
|
||||
"92": "Hauts-de-Seine",
|
||||
"93": "Seine-Saint-Denis",
|
||||
"94": "Val-de-Marne",
|
||||
"95": "Val-d'Oise",
|
||||
"971": "Guadeloupe",
|
||||
"972": "Martinique",
|
||||
"973": "Guyane",
|
||||
"974": "La Réunion",
|
||||
"976": "Mayotte",
|
||||
}
|
||||
22
config/custom_components/pollens/info.md
Normal file
22
config/custom_components/pollens/info.md
Normal file
@@ -0,0 +1,22 @@
|
||||
[](https://github.com/chris60600)
|
||||
[](https://github.com/custom-components/hacs)
|
||||
|
||||
# HomeAssistant - Pollens Custom Component
|
||||
|
||||
This module show pollen concentration inside [Homeassistant](https://home-assistant.io):
|
||||
|
||||
Datas provided by 'Réseau National de Surveillance Aérobiologique' (R.N.S.A.)
|
||||
https://pollens.fr
|
||||
|
||||
# Installation (There are two methods, with HACS or manual)
|
||||
|
||||
### 1. Easy Mode
|
||||
|
||||
|
||||
|
||||
### 2. Manual
|
||||
|
||||
Install it as you would do with any homeassistant custom component:
|
||||
|
||||
1. Download `custom_components` folder.
|
||||
2. Copy the `pollens` direcotry within the `custom_components` directory of your homeassistant installation. The `custom_components` directory resides within your homeassistant configuration directory.
|
||||
11
config/custom_components/pollens/manifest.json
Normal file
11
config/custom_components/pollens/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "pollens",
|
||||
"name": "Reseau National de Surveillance Aerobiologique ((R.N.S.A.))",
|
||||
"documentation": "https://github.com/chris60600/pollens-home-assistant",
|
||||
"issue_tracker": "https://github.com/chris60600/pollens-home-assistant/issues",
|
||||
"requirements": [],
|
||||
"codeowners": ["@chris60600"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"version": "2023.06.01"
|
||||
}
|
||||
69
config/custom_components/pollens/pollensasync.py
Normal file
69
config/custom_components/pollens/pollensasync.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""asyncio-friendly python API for RNSA (https://pollens.fr)."""
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from aiohttp.client import ClientError, ClientTimeout
|
||||
import json
|
||||
|
||||
import async_timeout
|
||||
|
||||
DEFAULT_TIMEOUT = 240
|
||||
|
||||
CLIENT_TIMEOUT = ClientTimeout(total=DEFAULT_TIMEOUT)
|
||||
|
||||
BASE_URL = "https://pollens.fr/risks/thea/counties/{}"
|
||||
|
||||
|
||||
class PollensClient:
|
||||
"""Pollens client implementation."""
|
||||
|
||||
def __init__(self, session: aiohttp.ClientSession = None, timeout=CLIENT_TIMEOUT):
|
||||
"""Constructor.
|
||||
session: aiohttp.ClientSession or None to create a new session.
|
||||
"""
|
||||
self._county_name = None
|
||||
self._params = {}
|
||||
self._risk_level = None
|
||||
self._risks = {}
|
||||
self._timeout = timeout
|
||||
if session is not None:
|
||||
self._session = session
|
||||
else:
|
||||
self._session = aiohttp.ClientSession()
|
||||
|
||||
async def Get(self, number):
|
||||
"""Get data by station number."""
|
||||
try:
|
||||
request = await self._session.get(
|
||||
BASE_URL.format(number), timeout=CLIENT_TIMEOUT
|
||||
)
|
||||
if "application/json" in request.headers["content-type"]:
|
||||
request_json = await request.json()
|
||||
else:
|
||||
request_json = await request.text()
|
||||
request_json = json.loads(request_json)
|
||||
|
||||
self._county_name = request_json["countyName"]
|
||||
for risk in request_json["risks"]:
|
||||
self._risks[risk["pollenName"]] = risk["level"]
|
||||
self._risk_level = request_json["riskLevel"]
|
||||
return request_json
|
||||
except (ClientError, asyncio.TimeoutError, ConnectionRefusedError) as err:
|
||||
return None
|
||||
|
||||
@property
|
||||
def county_name(self):
|
||||
return self._county_name
|
||||
|
||||
@property
|
||||
def risks(self):
|
||||
return self._risks
|
||||
|
||||
@property
|
||||
def risk_level(self):
|
||||
return self._risk_level
|
||||
|
||||
# async def _get(self, path, **kwargs):
|
||||
# with async_timeout.timeout(self._timeout):
|
||||
# resp = await self._session.get(path, params=dict(self._params, **kwargs))
|
||||
# print(type(resp.headers["Content-Type"]))
|
||||
# return await resp.text(encoding="utf-8")
|
||||
1
config/custom_components/pollens/requirement.txt
Normal file
1
config/custom_components/pollens/requirement.txt
Normal file
@@ -0,0 +1 @@
|
||||
async
|
||||
167
config/custom_components/pollens/sensor.py
Normal file
167
config/custom_components/pollens/sensor.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""Support for the RNSA pollens service."""
|
||||
|
||||
import logging
|
||||
from warnings import catch_warnings
|
||||
|
||||
from yaml import KeyToken
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
LIST_RISK,
|
||||
ATTR_URL,
|
||||
ATTR_COUNTY_NAME,
|
||||
ATTR_POLLEN_NAME,
|
||||
ATTR_LITERAL_STATE,
|
||||
KEY_TO_ATTR,
|
||||
COORDINATOR,
|
||||
CONF_POLLENSLIST,
|
||||
CONF_LITERAL,
|
||||
)
|
||||
from . import PollensEntity, PollensUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICONS = {
|
||||
0: "mdi:decagram-outline",
|
||||
1: "mdi:decagram-check",
|
||||
2: "mdi:alert-decagram-outline",
|
||||
3: "mdi:alert-decagram",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Setup Sensor Plateform"""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
|
||||
sensors = []
|
||||
try:
|
||||
enabled_pollens = entry.data[CONF_POLLENSLIST]
|
||||
except KeyError:
|
||||
enabled_pollens = [pollen for pollen in KEY_TO_ATTR]
|
||||
for risk in coordinator.api.risks:
|
||||
name = risk
|
||||
icon = KEY_TO_ATTR[risk.lower()][1]
|
||||
sensors.append(PollenSensor(coordinator, name=name, icon=icon, entry=entry, enabled=name.lower() in enabled_pollens))
|
||||
|
||||
name = f"pollens_{coordinator.county}"
|
||||
icon = ICONS[0]
|
||||
sensors.append(
|
||||
RiskSensor(coordinator=coordinator, name=name, icon=icon, entry=entry, numeric=False)
|
||||
)
|
||||
sensors.append(
|
||||
RiskSensor(coordinator=coordinator, name=name + "_risklevel", icon=icon, entry=entry, numeric=True)
|
||||
)
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class PollenSensor(PollensEntity, SensorEntity):
|
||||
"""Implementation of a Pollens sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PollensUpdateCoordinator,
|
||||
name: str,
|
||||
icon: str,
|
||||
entry: ConfigEntry,
|
||||
enabled: bool
|
||||
) -> None:
|
||||
super().__init__(coordinator, name, icon, entry)
|
||||
self._name = f"pollens_{coordinator.county}_{KEY_TO_ATTR[name.lower()][0]}"
|
||||
self._state = coordinator.api.risks[name]
|
||||
self._unique_id = f"{entry.entry_id}_{self._name}"
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = self._unique_id
|
||||
self._attr_entity_registry_enabled_default = enabled
|
||||
|
||||
try:
|
||||
self._literal_state = entry.data[CONF_LITERAL]
|
||||
# Setup DeviceClass in AQI and stateClass in numeric (Issue #15)
|
||||
if not self._literal_state:
|
||||
self._attr_device_class = SensorDeviceClass.AQI
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
except KeyError:
|
||||
self._literal_state = True
|
||||
self._attr_icon = icon
|
||||
self._friendly_name = f"{name}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
value = self.coordinator.api.risks[self._attr_name]
|
||||
if self._literal_state:
|
||||
return LIST_RISK[value]
|
||||
else:
|
||||
return value
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the last update."""
|
||||
attrs = {}
|
||||
attrs[ATTR_POLLEN_NAME] = self._friendly_name
|
||||
if self.coordinator.api.risks is not None:
|
||||
if not self._literal_state:
|
||||
value = self.coordinator.api.risks[self._attr_name]
|
||||
attrs[ATTR_LITERAL_STATE] = LIST_RISK[value]
|
||||
return attrs
|
||||
|
||||
|
||||
class RiskSensor(PollensEntity, SensorEntity):
|
||||
"""Implementation of Risk Sensor"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PollensUpdateCoordinator,
|
||||
name: str,
|
||||
icon: str,
|
||||
entry: ConfigEntry,
|
||||
numeric: bool
|
||||
) -> None:
|
||||
super().__init__(coordinator, name, icon, entry)
|
||||
self._risk_level = coordinator.api.risk_level
|
||||
self._attr_unique_id = f"{entry.entry_id}_{coordinator.county}"
|
||||
self._attr_icon = icon
|
||||
self._name = name
|
||||
self._numeric = numeric
|
||||
if numeric:
|
||||
self._attr_unique_id += "_risklevel"
|
||||
self._attr_device_class = SensorDeviceClass.AQI
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
value = self.coordinator.api.risk_level
|
||||
if self._numeric:
|
||||
return value
|
||||
else:
|
||||
return LIST_RISK[value]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return ICONS[self._risk_level]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
attrs = {}
|
||||
attrs[ATTR_URL] = "https://pollens.fr"
|
||||
attrs[ATTR_COUNTY_NAME] = self.coordinator.api.county_name
|
||||
return attrs
|
||||
45
config/custom_components/pollens/strings.json
Normal file
45
config/custom_components/pollens/strings.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "Pollens",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title":"Pollens - Step #1",
|
||||
"description":"If you need information about pollens on site R.N.S.A. website, have a look here: https://www.pollens.fr",
|
||||
"data": {
|
||||
"county": "[%key:common::config_flow::data::county%]",
|
||||
"scan_interval": "[%key:common::config_flow::data::scan_interval%]",
|
||||
"literal_states":"[%key:common::config_flow::data::literal_states%]"
|
||||
}
|
||||
},
|
||||
"select_pollens": {
|
||||
"title":"Pollens - Step #2",
|
||||
"description":"Select pollens \r\nhttps://www.pollens.fr",
|
||||
"data": {
|
||||
"pollens_list": "Select pollens from list"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"invalid_county": "[%key:common::config_flow::error::invalid_county%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"options":{
|
||||
"step":{
|
||||
"init":{
|
||||
"data":{
|
||||
"scan_interval": "Scan interval"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_scan_interval":"[%key:common::config_flow::error::invalid_scan_interval%]"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
46
config/custom_components/pollens/translations/en.json
Normal file
46
config/custom_components/pollens/translations/en.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"title": "Pollens (from R.N.S.A. website)\r\nPlease visit www.pollen.fr for more information",
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"single_instance_allowed": "Only one configuration of Pollens allowed"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_county": "Only one county is actualy allowed",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"title":"Pollens - Step #1",
|
||||
"description":"Select county \r\nhttps://www.pollens.fr",
|
||||
"data": {
|
||||
"county": "County",
|
||||
"scan_interval": "Scan interval (hours)",
|
||||
"literal_states":"States in literal (in numeric if not selected)"
|
||||
}
|
||||
},
|
||||
"select_pollens": {
|
||||
"title":"Pollens - Step #2",
|
||||
"description":"Select pollens \r\nhttps://www.pollens.fr",
|
||||
"data": {
|
||||
"pollens_list": "Select pollens from list"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options":{
|
||||
"step":{
|
||||
"init":{
|
||||
"data":{
|
||||
"scan_interval": "Scan interval (hours)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_scan_interval": "Invalid scan interval. Must be higher than 1 hour"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
45
config/custom_components/pollens/translations/fr.json
Normal file
45
config/custom_components/pollens/translations/fr.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "Pollens",
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"single_instance_allowed": "Une seule configuration de Pollens est autoris\u00e9e"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossible de se connecter au serveur",
|
||||
"invalid_county": "Un seul d\u00e9partment est actuellemnt g\u00e9r\u00e9",
|
||||
"unknown": "Erreur inconnue"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"county": "D\u00e9partement",
|
||||
"scan_interval": "P\u00e9riode d\u0027int\u00e9rogation (heures)",
|
||||
"literal_states": "Etats en texte (non selection\u00e9 = num\u00e9rique)"
|
||||
},
|
||||
"title":"Pollens - Etape #1",
|
||||
"description":"Si vous avez besoin d\u0027information sur les pollens rendez vous sur le site du R.N.S.A, https://www.pollens.fr"
|
||||
},
|
||||
"select_pollens": {
|
||||
"title":"Pollens - Etape #2",
|
||||
"description":"Selection des pollens \r\nhttps://www.pollens.fr",
|
||||
"data": {
|
||||
"pollens_list": "Selectionnez un/des pollens dans la liste"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options":{
|
||||
"step":{
|
||||
"init":{
|
||||
"data":{
|
||||
"scan_interval": "P\u00e9riode d\u0027int\u00e9rogation (heures)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_scan_interval": "La periode d interrogation doit etre superieure a 1 heure"
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user