Home Assistant Git Exporter
This commit is contained in:
401
config/custom_components/vigieau/__init__.py
Normal file
401
config/custom_components/vigieau/__init__.py
Normal file
@@ -0,0 +1,401 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import urllib.parse
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from dateutil import tz
|
||||
from itertools import dropwhile, takewhile
|
||||
import aiohttp
|
||||
|
||||
|
||||
from homeassistant.const import Platform, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.components.sensor import RestoreSensor, SensorEntity
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SENSOR_DEFINITIONS,
|
||||
CONF_INSEE_CODE,
|
||||
CONF_CITY,
|
||||
CONF_LOCATION_MODE,
|
||||
DEVICE_ID_KEY,
|
||||
HA_COORD,
|
||||
NAME,
|
||||
SENSOR_DEFINITIONS,
|
||||
LOCATION_MODES,
|
||||
VigieEauSensorEntityDescription,
|
||||
)
|
||||
from .config_flow import get_insee_code_fromcoord
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from .api import VigieauApi, VigieauApiError
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIGRATED_FROM_VERSION_1 = "migrated_from_version_1"
|
||||
MIGRATED_FROM_VERSION_3 = "migrated_from_version_3"
|
||||
|
||||
|
||||
async def async_migrate_entry(hass, config_entry: ConfigEntry):
|
||||
if config_entry.version == 1:
|
||||
_LOGGER.warn("config entry version is 1, migrating to version 2")
|
||||
new = {**config_entry.data}
|
||||
insee_code, city_name, lat, lon = await get_insee_code_fromcoord(hass)
|
||||
new[CONF_INSEE_CODE] = insee_code
|
||||
new[CONF_CITY] = city_name
|
||||
new[CONF_LOCATION_MODE] = HA_COORD
|
||||
new[
|
||||
DEVICE_ID_KEY
|
||||
] = "Vigieau" # hardcoded to match hardcoded id from version 0.3.9
|
||||
new[CONF_LATITUDE] = lat
|
||||
new[CONF_LONGITUDE] = lon
|
||||
new[MIGRATED_FROM_VERSION_1] = True
|
||||
_LOGGER.warn(
|
||||
f"Migration detected insee code for current HA instance is {insee_code} in {city_name}"
|
||||
)
|
||||
|
||||
config_entry.version = 3
|
||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||
if config_entry.version == 2:
|
||||
_LOGGER.warn("config entry version is 2, migrating to version 3")
|
||||
new = {**config_entry.data}
|
||||
insee_code, city_name, lat, lon = await get_insee_code_fromcoord(hass)
|
||||
new[CONF_LATITUDE] = lat
|
||||
new[CONF_LONGITUDE] = lon
|
||||
config_entry.version = 3
|
||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||
|
||||
if config_entry.version == 3:
|
||||
_LOGGER.warn("config entry version is 3, migrating to version 4")
|
||||
new = {**config_entry.data}
|
||||
insee_code, city_name, lat, lon = await get_insee_code_fromcoord(hass)
|
||||
new[MIGRATED_FROM_VERSION_3] = True
|
||||
config_entry.version = 4
|
||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# here we store the coordinator for future access
|
||||
if entry.entry_id not in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN][entry.entry_id] = {}
|
||||
hass.data[DOMAIN][entry.entry_id]["vigieau_coordinator"] = VigieauAPICoordinator(
|
||||
hass, dict(entry.data)
|
||||
)
|
||||
|
||||
# will make sure async_setup_entry from sensor.py is called
|
||||
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
|
||||
|
||||
# subscribe to config updates
|
||||
entry.async_on_unload(entry.add_update_listener(update_entry))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def update_entry(hass, entry):
|
||||
"""
|
||||
This method is called when options are updated
|
||||
We trigger the reloading of entry (that will eventually call async_unload_entry)
|
||||
"""
|
||||
_LOGGER.debug("update_entry method called")
|
||||
# will make sure async_setup_entry from sensor.py is called
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""This method is called to clean all sensors before re-adding them"""
|
||||
_LOGGER.debug("async_unload_entry method called")
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
entry, [Platform.SENSOR]
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
class VigieauAPICoordinator(DataUpdateCoordinator):
|
||||
"""A coordinator to fetch data from the api only once"""
|
||||
|
||||
def __init__(self, hass, config: ConfigType):
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="vigieau api", # for logging purpose
|
||||
update_interval=timedelta(hours=1),
|
||||
update_method=self.update_method,
|
||||
)
|
||||
self.config = config
|
||||
self.hass = hass
|
||||
|
||||
async def update_method(self):
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
_LOGGER.debug(
|
||||
f"Calling update method, {len(self._listeners)} listeners subscribed"
|
||||
)
|
||||
if "VIGIEAU_APIFAIL" in os.environ:
|
||||
raise UpdateFailed(
|
||||
"Failing update on purpose to test state restoration"
|
||||
)
|
||||
_LOGGER.debug("Starting collecting data")
|
||||
|
||||
city_code = self.config[CONF_INSEE_CODE]
|
||||
lat = self.config[CONF_LATITUDE]
|
||||
long = self.config[CONF_LONGITUDE]
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
vigieau = VigieauApi(session)
|
||||
try:
|
||||
# TODO(kamaradclimber): there 4 supported profils: particulier, entreprise, collectivite and exploitation
|
||||
data = await vigieau.get_data(lat, long, city_code, "particulier")
|
||||
except VigieauApiError as e:
|
||||
raise UpdateFailed(f"Failed fetching vigieau data: {e.text}")
|
||||
|
||||
for usage in data["usages"]:
|
||||
found = False
|
||||
for sensor in SENSOR_DEFINITIONS:
|
||||
for matcher in sensor.matchers:
|
||||
if re.search(
|
||||
matcher,
|
||||
usage["nom"] + "|" + usage['thematique'],
|
||||
re.IGNORECASE,
|
||||
):
|
||||
found = True
|
||||
if not found:
|
||||
report_data = json.dumps(
|
||||
{"insee code": city_code, "nom": usage["nom"]},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
_LOGGER.warn(
|
||||
f"The following restriction is unknown from this integration, please report an issue with: {report_data}"
|
||||
)
|
||||
return data
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}")
|
||||
|
||||
|
||||
class AlertLevelEntity(CoordinatorEntity, SensorEntity):
|
||||
"""Expose the alert level for the location"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: VigieauAPICoordinator,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
):
|
||||
super().__init__(coordinator)
|
||||
self.hass = hass
|
||||
self._attr_name = f"Alert level in {config_entry.data.get(CONF_CITY)}"
|
||||
self._attr_native_value = None
|
||||
self._attr_state_attributes = None
|
||||
if MIGRATED_FROM_VERSION_1 in config_entry.data:
|
||||
self._attr_unique_id = "sensor-vigieau-Alert level"
|
||||
else:
|
||||
self._attr_unique_id = f"sensor-vigieau-{self._attr_name}-{config_entry.data.get(CONF_INSEE_CODE)}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=f"{NAME} {config_entry.data.get(CONF_CITY)}",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
str(config_entry.data.get(DEVICE_ID_KEY)),
|
||||
)
|
||||
},
|
||||
manufacturer=NAME,
|
||||
model=config_entry.data.get(CONF_INSEE_CODE),
|
||||
)
|
||||
|
||||
def enrich_attributes(self, data: dict, key_source: str, key_target: str):
|
||||
if key_source in data:
|
||||
self._attr_state_attributes = self._attr_state_attributes or {}
|
||||
if key_source in data:
|
||||
self._attr_state_attributes[key_target] = data[key_source]
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
_LOGGER.debug(f"Receiving an update for {self.unique_id} sensor")
|
||||
if not self.coordinator.last_update_success:
|
||||
_LOGGER.debug("Last coordinator failed, assuming state has not changed")
|
||||
return
|
||||
self._attr_native_value = self.coordinator.data["niveauGravite"]
|
||||
|
||||
self._attr_icon = {
|
||||
"vigilance": "mdi:water-check",
|
||||
"alerte": "mdi:water-alert",
|
||||
"alerte_renforcée": "mdi:water-remove",
|
||||
"alerte_renforcee": "mdi:water-remove",
|
||||
"crise": "mdi:water-off",
|
||||
}[self._attr_native_value.lower().replace(" ", "_")]
|
||||
|
||||
self.enrich_attributes(self.coordinator.data, "cheminFichier", "source")
|
||||
self.enrich_attributes(
|
||||
self.coordinator.data, "cheminFichierArreteCadre", "source2"
|
||||
)
|
||||
|
||||
restrictions = [
|
||||
restriction["nom"] for restriction in self.coordinator.data["usages"]
|
||||
]
|
||||
self._attr_state_attributes = self._attr_state_attributes or {}
|
||||
self._attr_state_attributes["current_restrictions"] = ", ".join(restrictions)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
return self._attr_state_attributes
|
||||
|
||||
|
||||
class UsageRestrictionEntity(CoordinatorEntity, SensorEntity):
|
||||
"""Expose a restriction for a given usage"""
|
||||
|
||||
entity_description: VigieEauSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: VigieauAPICoordinator,
|
||||
hass: HomeAssistant,
|
||||
usage_id: str,
|
||||
config_entry: ConfigEntry,
|
||||
description: VigieEauSensorEntityDescription,
|
||||
):
|
||||
super().__init__(coordinator)
|
||||
self.hass = hass
|
||||
# naming the attribute very early before it's updated by first api response is a hack
|
||||
# to make sure we have a decent entity_id selected by home assistant
|
||||
self._attr_name = (
|
||||
f"{description.name}_restrictions_{config_entry.data.get(CONF_CITY)}"
|
||||
)
|
||||
self._attr_native_value = None
|
||||
self._attr_state_attributes = None
|
||||
self._attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
self._config = description
|
||||
if MIGRATED_FROM_VERSION_1 in config_entry.data:
|
||||
self._attr_unique_id = f"sensor-vigieau-{self._config.key}"
|
||||
elif MIGRATED_FROM_VERSION_3 in config_entry.data:
|
||||
self._attr_unique_id = f"sensor-vigieau-{self._attr_name}-{config_entry.data.get(CONF_INSEE_CODE)}-{config_entry.data.get(CONF_LATITUDE)}-{config_entry.data.get(CONF_LONGITUDE)}"
|
||||
else:
|
||||
self._attr_unique_id = f"sensor-vigieau-{self._config.key}-{config_entry.data.get(CONF_INSEE_CODE)}-{config_entry.data.get(CONF_LATITUDE)}-{config_entry.data.get(CONF_LONGITUDE)}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=f"{NAME} {config_entry.data.get(CONF_CITY)}",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
str(config_entry.data.get(DEVICE_ID_KEY)),
|
||||
)
|
||||
},
|
||||
manufacturer=NAME,
|
||||
model=config_entry.data.get(CONF_INSEE_CODE),
|
||||
)
|
||||
|
||||
def enrich_attributes(self, usage: dict, key_source: str, key_target: str):
|
||||
if key_source in usage:
|
||||
self._attr_state_attributes = self._attr_state_attributes or {}
|
||||
self._attr_state_attributes[key_target] = usage[key_source]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return self._config.icon
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
_LOGGER.debug(f"Receiving an update for {self.unique_id} sensor")
|
||||
if not self.coordinator.last_update_success:
|
||||
_LOGGER.debug("Last coordinator failed, assuming state has not changed")
|
||||
return
|
||||
|
||||
self._attr_state_attributes = self._attr_state_attributes or {}
|
||||
self._restrictions = []
|
||||
self._time_restrictions = {}
|
||||
self._attr_name = str(self._config.name)
|
||||
for usage in self.coordinator.data["usages"]:
|
||||
for matcher in self._config.matchers:
|
||||
fully_qualified_usage = usage["nom"] + "|" + usage['thematique']
|
||||
if re.search(matcher, fully_qualified_usage, re.IGNORECASE):
|
||||
self._attr_state_attributes = self._attr_state_attributes or {}
|
||||
restriction = usage.get("description")
|
||||
if restriction is None:
|
||||
raise UpdateFailed(
|
||||
"Restriction level is not specified"
|
||||
)
|
||||
self._attr_state_attributes[
|
||||
f"Categorie: {usage['nom']}"
|
||||
] = restriction
|
||||
self._restrictions.append(restriction)
|
||||
|
||||
self.enrich_attributes(
|
||||
usage, "details", f"{usage['nom']} (details)"
|
||||
)
|
||||
if "heureFin" in usage and "heureDebut" in usage:
|
||||
self._time_restrictions[usage["nom"]] = [
|
||||
usage["heureDebut"],
|
||||
usage["heureFin"],
|
||||
]
|
||||
|
||||
# we only want to add those attributes if they are not ambiguous
|
||||
if len(set([repr(r) for r in self._time_restrictions.values()])) == 1:
|
||||
restrictions = list(self._time_restrictions.values())[0]
|
||||
self._attr_state_attributes["heureDebut"] = restrictions[0]
|
||||
self._attr_state_attributes["heureFin"] = restrictions[1]
|
||||
elif len(self._time_restrictions) > 0:
|
||||
_LOGGER.debug(
|
||||
f"There are {len(self._time_restrictions)} usage with time restrictions for this sensor, exposing info per usage"
|
||||
)
|
||||
for name in self._time_restrictions:
|
||||
self._attr_state_attributes[
|
||||
f"{name} (heureDebut)"
|
||||
] = self._time_restrictions[name][0]
|
||||
self._attr_state_attributes[
|
||||
f"{name} (heureFin)"
|
||||
] = self._time_restrictions[name][1]
|
||||
|
||||
self._attr_native_value = self.compute_native_value()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def compute_native_value(self) -> Optional[str]:
|
||||
"""This method extract the most relevant restriction level to display as aggregate"""
|
||||
if len(self._restrictions) == 0:
|
||||
return "Aucune restriction"
|
||||
if "Interdiction sur plage horaire" in self._restrictions:
|
||||
return "Interdiction sur plage horaire"
|
||||
if "Interdiction sauf exception" in self._restrictions:
|
||||
return "Interdiction sauf exception"
|
||||
if "Interdit sauf pour les usages commerciaux après accord du service de police de l’eau." in self._restrictions:
|
||||
return "Interdiction sauf exception"
|
||||
if "Interdiction" in self._restrictions:
|
||||
return "Interdiction"
|
||||
if "Interdiction." in self._restrictions:
|
||||
return "Interdiction"
|
||||
if "Réduction de prélèvement" in self._restrictions:
|
||||
return "Réduction de prélèvement"
|
||||
if "Consulter l’arrêté" in self._restrictions:
|
||||
return "Erreur: consulter l'arreté"
|
||||
if "Se référer à l'arrêté de restriction en cours de validité." in self._restrictions:
|
||||
return "Erreur: consulter l'arreté"
|
||||
if "Pas de restriction sauf arrêté spécifique." in self._restrictions:
|
||||
return "Autorisé sauf exception"
|
||||
if len(self._restrictions) == 1:
|
||||
return self._restrictions[0]
|
||||
_LOGGER.warn(f"Restrictions are hard to interpret: {self._restrictions}")
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
return self._attr_state_attributes
|
||||
Reference in New Issue
Block a user