From cbac6ed8bee3f9b7575a2912039c64dd10919100 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Thu, 8 Feb 2024 06:15:09 +0100 Subject: [PATCH 1/6] Rewrite integration to reduce number of API calls --- custom_components/antistorm/__init__.py | 61 ++++++- custom_components/antistorm/binary_sensor.py | 147 ++++++++-------- custom_components/antistorm/config_flow.py | 46 +++++ custom_components/antistorm/connector.py | 38 +++++ custom_components/antistorm/const.py | 21 +++ custom_components/antistorm/entity.py | 41 +++++ custom_components/antistorm/manifest.json | 9 +- custom_components/antistorm/sensor.py | 159 +++++++++--------- .../antistorm/translations/en.json | 42 +++++ .../antistorm/translations/pl.json | 42 +++++ .../antistorm/update_coordinator.py | 23 +++ 11 files changed, 468 insertions(+), 161 deletions(-) create mode 100644 custom_components/antistorm/config_flow.py create mode 100644 custom_components/antistorm/connector.py create mode 100644 custom_components/antistorm/const.py create mode 100644 custom_components/antistorm/entity.py create mode 100644 custom_components/antistorm/translations/en.json create mode 100644 custom_components/antistorm/translations/pl.json create mode 100644 custom_components/antistorm/update_coordinator.py diff --git a/custom_components/antistorm/__init__.py b/custom_components/antistorm/__init__.py index bdf482a..1507f19 100644 --- a/custom_components/antistorm/__init__.py +++ b/custom_components/antistorm/__init__.py @@ -1 +1,60 @@ -"""Antistorm""" \ No newline at end of file +import logging + +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import ( + DOMAIN, PLATFORMS, CONF_CITY_ID +) +from .update_coordinator import AntistormUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_CITY_ID): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + } +) + + +async def async_setup(_hass, _config): + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + if hass.data.get(DOMAIN) is None: + hass.data.setdefault(DOMAIN, {}) + + city_id = config_entry.data.get(CONF_CITY_ID) + + coordinator = AntistormUpdateCoordinator(hass, city_id) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + unloaded = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + if unloaded: + hass.data[DOMAIN].pop(config_entry.entry_id) + return unloaded + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) diff --git a/custom_components/antistorm/binary_sensor.py b/custom_components/antistorm/binary_sensor.py index c967a73..4658c83 100644 --- a/custom_components/antistorm/binary_sensor.py +++ b/custom_components/antistorm/binary_sensor.py @@ -1,89 +1,80 @@ import logging -import requests +from dataclasses import dataclass +from typing import Callable -import voluptuous as vol +from homeassistant.config_entries import ConfigEntry -from homeassistant.components.binary_sensor import DEVICE_CLASS_SAFETY, PLATFORM_SCHEMA, ENTITY_ID_FORMAT -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, ATTR_ATTRIBUTION -import homeassistant.helpers.config_validation as cv -try: - from homeassistant.components.binary_sensor import BinarySensorEntity -except ImportError: - from homeassistant.components.binary_sensor import BinarySensorDevice as BinarySensorEntity -from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.core import HomeAssistant +from homeassistant.components.binary_sensor import BinarySensorDeviceClass, BinarySensorEntityDescription, DOMAIN as BS_DOMAIN +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback -_LOGGER = logging.getLogger(__name__) - -CONF_STATION_ID = 'station_id' - -DEFAULT_NAME = 'Antistorm' -ATTRIBUTION = 'Information provided by Antistorm.eu.' - -SENSOR_TYPES = { - 'storm_alarm': ['a_b', 'Alarm burzowy', 'mdi:weather-lightning'], - 'rain_alarm': ['a_o', 'Alarm opadów', 'mdi:weather-pouring'], - 'storm_active': ['s', 'Aktywna burza', 'mdi:weather-lightning-rainy'], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_STATION_ID): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - station_id = config.get(CONF_STATION_ID) - name = config.get(CONF_NAME) - address = 'http://antistorm.eu/webservice.php?id=' + str(station_id) - request = requests.get(address) - request.encoding = 'utf-8' - city_name = request.json()['m'] - dev = [] - for monitored_condition in config[CONF_MONITORED_CONDITIONS]: - uid = '{}_{}_{}'.format(name, station_id, monitored_condition) - entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, hass=hass) - dev.append(AntistormBinarySensor(entity_id, name, city_name, monitored_condition, station_id)) - add_entities(dev, True) - - -class AntistormBinarySensor(BinarySensorEntity): - def __init__(self, entity_id, name, city_name, sensor_type, station_id): - self.entity_id = entity_id - self._name = name - self.city_name = city_name - self.station_id = station_id - self.sensor_type = sensor_type - self.data = None - self._state = None - self._jsonParameter = SENSOR_TYPES[sensor_type][0] - self._name = SENSOR_TYPES[sensor_type][1] +from .const import DOMAIN +from .update_coordinator import AntistormUpdateCoordinator, AntistormData +from .entity import AntistormEntity - @property - def extra_state_attributes(self): - output = dict() - output[ATTR_ATTRIBUTION] = ATTRIBUTION - return output +_LOGGER = logging.getLogger(__name__) - @property - def name(self): - return '{} {} - {}'.format(self._name, self.city_name, SENSOR_TYPES[self.sensor_type][1]) - @property - def icon(self): - return SENSOR_TYPES[self.sensor_type][2] +@dataclass(frozen=True) +class AntistormBinarySensorDescriptionMixin: + value_fn: Callable[[AntistormData], bool] + + +@dataclass(frozen=True) +class AntistormBinarySensorEntityDescription(BinarySensorEntityDescription, AntistormBinarySensorDescriptionMixin): + device_class = BinarySensorDeviceClass.SAFETY + has_entity_name = True + + +entity_descriptions = [ + AntistormBinarySensorEntityDescription( + key='storm_alarm', + translation_key='storm_alarm', + icon="mdi:weather-lightning", + value_fn=lambda data: data.storm_alarm, + ), + AntistormBinarySensorEntityDescription( + key='rain_alarm', + translation_key='rain_alarm', + icon="mdi:weather-pouring", + value_fn=lambda data: data.precipitation_alarm, + ), + AntistormBinarySensorEntityDescription( + key='storm_active', + translation_key='storm_active', + icon="mdi:weather-lightning-rainy", + value_fn=lambda data: data.storm_active, + ), +] + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback) -> bool: + coordinator: AntistormUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + for entity_description in entity_descriptions: + entities.append(AntistormBinarySensor(coordinator, config_entry, entity_description)) + async_add_entities(entities) + return True + + +class AntistormBinarySensor(AntistormEntity, BinarySensorEntity): + entity_description: AntistormBinarySensorEntityDescription + + def __init__(self, coordinator: AntistormUpdateCoordinator, config_entry: ConfigEntry, + description: AntistormBinarySensorEntityDescription) -> None: + super().__init__(coordinator, config_entry) + self.entity_description = description + self._attr_unique_id = f"{DOMAIN}_{description.key}" + self.entity_id = f"{BS_DOMAIN}.{DOMAIN}_{self.city_id}_{description.key}" @property - def is_on(self): - return self.data is not None and int(self.data[self._jsonParameter]) > 0 + def is_on(self) -> bool | None: + if self.get_data() is None: + return None + return self.entity_description.value_fn(self.get_data()) @property - def device_class(self): - return DEVICE_CLASS_SAFETY - - def update(self): - address = 'http://antistorm.eu/webservice.php?id=' + str(self.station_id) - request = requests.get(address) - if request.status_code == 200 and request.content.__len__() > 0: - self.data = request.json() + def unique_id(self) -> str: + return f"{super().unique_id}_{self.entity_description.key}" diff --git a/custom_components/antistorm/config_flow.py b/custom_components/antistorm/config_flow.py new file mode 100644 index 0000000..5edcdbb --- /dev/null +++ b/custom_components/antistorm/config_flow.py @@ -0,0 +1,46 @@ +"""Config flow to configure Antistorm integration.""" + +import logging + +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult + +from .connector import AntistormConnector +from .const import CONF_CITY_ID, CONF_CITY_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA_CITY_ID = vol.Schema({ + vol.Required(CONF_CITY_NAME): str, +}) + +class AntistormFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 1 + + async def get_city_details(self, city_name: str) -> tuple[int, str] | None: + city_id = await self.hass.async_add_executor_job(AntistormConnector.get_city_id, city_name) + if city_id is None: + return None + city_name = (await self.hass.async_add_executor_job(lambda: AntistormConnector(city_id).get_data())).city + return city_id, city_name + + async def async_step_user(self, user_input=None) -> FlowResult: + errors = {} + if user_input is not None: + usr_city_name = user_input[CONF_CITY_NAME] + details = await self.get_city_details(usr_city_name) + if details is not None: + city_id = details[0] + city_name = details[1] + return self.async_create_entry( + title= city_name, + data={ + CONF_CITY_ID: city_id, + CONF_NAME: city_name + }, + ) + else: + errors[CONF_CITY_NAME] = "city_not_found" + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_CITY_ID, errors=errors) diff --git a/custom_components/antistorm/connector.py b/custom_components/antistorm/connector.py new file mode 100644 index 0000000..a1db7fe --- /dev/null +++ b/custom_components/antistorm/connector.py @@ -0,0 +1,38 @@ +import logging +import requests + +from .const import API_URL, GET_CITY_URL + +_LOGGER = logging.getLogger(__name__) + + +class AntistormData: + def __init__(self, m: str, p_b: int, t_b: int, a_b: int, p_o: int, t_o: int, a_o: int, s: int) -> None: + self.city = m + self.storm_probability = p_b + self.storm_time = t_b + self.storm_alarm = a_b == 1 + self.storm_active = s == 1 + self.precipitation_probability = p_o + self.precipitation_time = t_o + self.precipitation_alarm = a_o == 1 + + +class AntistormConnector: + def __init__(self, city_id: int) -> None: + self._city_id = city_id + + def get_data(self) -> AntistormData: + response = requests.get(f"{API_URL}{self._city_id}") + response.encoding = 'utf-8' + if response.status_code != 200: + raise Exception(f"Error ({response.status_code}) getting Antistorm data: {response.text}") + data = AntistormData(**response.json()) + return data + + @staticmethod + def get_city_id(city_name: str) -> int | None: + response = requests.post(GET_CITY_URL, data={"miasto": city_name}) + if response.status_code != 200 or response.text == "-1": + return None + return int(response.text) diff --git a/custom_components/antistorm/const.py b/custom_components/antistorm/const.py new file mode 100644 index 0000000..f7ae3b7 --- /dev/null +++ b/custom_components/antistorm/const.py @@ -0,0 +1,21 @@ +from typing import Final +from datetime import timedelta + +from homeassistant.const import Platform + +DOMAIN: Final = "antistorm" +DEFAULT_NAME: Final = "Antistorm" +DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=5) +BASE_URL: Final = 'https://antistorm.eu' +API_URL: Final = f"{BASE_URL}/webservice.php?id=" +GET_CITY_URL: Final = f"{BASE_URL}/api-v1/szukaj-miasta.php" + +ATTRIBUTION = 'Information provided by Antistorm.eu.' + +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, +] + +CONF_CITY_ID = "city_id" +CONF_CITY_NAME = "city_name" diff --git a/custom_components/antistorm/entity.py b/custom_components/antistorm/entity.py new file mode 100644 index 0000000..4528083 --- /dev/null +++ b/custom_components/antistorm/entity.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .update_coordinator import AntistormUpdateCoordinator +from .connector import AntistormData +from .const import ATTRIBUTION, DEFAULT_NAME, DOMAIN, CONF_CITY_ID, API_URL + + +class AntistormEntity(CoordinatorEntity): + + def __init__(self, coordinator: AntistormUpdateCoordinator, config_entry: ConfigEntry): + super().__init__(coordinator) + self.config_entry = config_entry + self.city_id = config_entry.data[CONF_CITY_ID] + + @property + def extra_state_attributes(self) -> dict: + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + def get_data(self) -> AntistormData | None: + return self.coordinator.data + + def base_name(self) -> str: + return f"{DEFAULT_NAME} {self.config_entry.data[CONF_NAME]}" + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.config_entry.data[CONF_CITY_ID]}" + + @property + def device_info(self) -> DeviceInfo: + city_id = self.config_entry.data[CONF_CITY_ID] + return { + "identifiers": {(DOMAIN, city_id)}, + "name": self.base_name(), + "configuration_url": API_URL, + } diff --git a/custom_components/antistorm/manifest.json b/custom_components/antistorm/manifest.json index 9a6e7db..498f9cc 100644 --- a/custom_components/antistorm/manifest.json +++ b/custom_components/antistorm/manifest.json @@ -1,11 +1,12 @@ { "domain": "antistorm", "name": "Antistorm", + "codeowners": ["@PiotrMachowski"], + "config_flow": true, + "dependencies": [], "documentation": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Antistorm", + "iot_class": "cloud_polling", "issue_tracker": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Antistorm/issues", - "dependencies": [], - "codeowners": ["@PiotrMachowski"], "requirements": ["requests"], - "version": "v1.0.4", - "iot_class": "cloud_polling" + "version": "v2.0.0" } \ No newline at end of file diff --git a/custom_components/antistorm/sensor.py b/custom_components/antistorm/sensor.py index 1d71bbc..19fe482 100644 --- a/custom_components/antistorm/sensor.py +++ b/custom_components/antistorm/sensor.py @@ -1,90 +1,93 @@ import logging -import requests +from dataclasses import dataclass +from typing import Callable -import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTime -from homeassistant.components.sensor import (PLATFORM_SCHEMA, ENTITY_ID_FORMAT) -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, ATTR_ATTRIBUTION -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -_LOGGER = logging.getLogger(__name__) -CONF_STATION_ID = 'station_id' - -DEFAULT_NAME = 'Antistorm' -ATTRIBUTION = 'Information provided by Antistorm.eu.' - -SENSOR_TYPES = { - 'storm_probability': ['p_b', 'Prawdopodobieństwo burzy', ' ', 'mdi:weather-lightning'], - 'storm_time': ['t_b', 'Czas do burzy', ' ', 'mdi:weather-lightning'], - 'rain_probability': ['p_o', 'Prawdopodobieństwo opadów', ' ', 'mdi:weather-pouring'], - 'rain_time': ['t_o', 'Czas do opadów', ' ', 'mdi:weather-pouring'], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_STATION_ID): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - station_id = config.get(CONF_STATION_ID) - name = config.get(CONF_NAME) - address = 'http://antistorm.eu/webservice.php?id=' + str(station_id) - request = requests.get(address) - request.encoding = 'utf-8' - city_name = request.json()['m'] - dev = [] - for monitored_condition in config[CONF_MONITORED_CONDITIONS]: - uid = '{}_{}_{}'.format(DEFAULT_NAME, station_id, monitored_condition) - entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, hass=hass) - dev.append(AntistormSensor(entity_id, name, city_name, monitored_condition, station_id)) - add_entities(dev, True) - - -class AntistormSensor(Entity): - def __init__(self, entity_id, name, city_name, sensor_type, station_id): - self.entity_id = entity_id - self._name = name - self.city_name = city_name - self.station_id = station_id - self.sensor_type = sensor_type - self.data = None - self._state = None - self._jsonParameter = SENSOR_TYPES[sensor_type][0] - self._name = SENSOR_TYPES[sensor_type][1] - self._unit_of_measurement = SENSOR_TYPES[sensor_type][2] +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription, DOMAIN as S_DOMAIN +from homeassistant.helpers.typing import StateType - @property - def extra_state_attributes(self): - output = dict() - output[ATTR_ATTRIBUTION] = ATTRIBUTION - return output +from .const import DOMAIN +from .update_coordinator import AntistormUpdateCoordinator, AntistormData +from .entity import AntistormEntity - @property - def name(self): - return '{} {} - {}'.format(self._name, self.city_name, SENSOR_TYPES[self.sensor_type][1]) - @property - def icon(self): - return SENSOR_TYPES[self.sensor_type][3] +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class AntistormSensorDescriptionMixin: + value_fn: Callable[[AntistormData], int] + + +@dataclass(frozen=True) +class AntistormSensorEntityDescription(SensorEntityDescription, AntistormSensorDescriptionMixin): + has_entity_name = True + + +entity_descriptions = [ + AntistormSensorEntityDescription( + key='storm_probability', + translation_key='storm_probability', + icon="mdi:weather-lightning", + native_unit_of_measurement=' ', + value_fn=lambda data: data.storm_probability, + ), + AntistormSensorEntityDescription( + key='storm_time', + translation_key='storm_time', + icon="mdi:weather-lightning", + native_unit_of_measurement=UnitOfTime.MINUTES, + value_fn=lambda data: data.storm_time, + ), + AntistormSensorEntityDescription( + key='precipitation_probability', + translation_key='precipitation_probability', + icon="mdi:weather-pouring", + native_unit_of_measurement=' ', + value_fn=lambda data: data.precipitation_probability, + ), + AntistormSensorEntityDescription( + key='precipitation_time', + translation_key='precipitation_time', + icon="mdi:weather-pouring", + native_unit_of_measurement=UnitOfTime.MINUTES, + value_fn=lambda data: data.precipitation_time, + ) +] + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback) -> bool: + coordinator: AntistormUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + for entity_description in entity_descriptions: + entities.append(AntistormSensor(coordinator, config_entry, entity_description)) + async_add_entities(entities) + return True + + +class AntistormSensor(AntistormEntity, SensorEntity): + entity_description: AntistormSensorEntityDescription + + def __init__(self, coordinator: AntistormUpdateCoordinator, config_entry: ConfigEntry, + description: AntistormSensorEntityDescription) -> None: + super().__init__(coordinator, config_entry) + self.entity_description = description + self._attr_unique_id = f"{DOMAIN}_{description.key}" + self.entity_id = f"{S_DOMAIN}.{DOMAIN}_{self.city_id}_{description.key}" @property - def state(self): - if self.data is not None: - self._state = self.data[self._jsonParameter] - return self._state + def unique_id(self) -> str: + return f"{super().unique_id}_{self.entity_description.key}" @property - def unit_of_measurement(self): - return self._unit_of_measurement - - def update(self): - address = 'http://antistorm.eu/webservice.php?id=' + str(self.station_id) - request = requests.get(address) - if request.status_code == 200 and request.content.__len__() > 0: - self.data = request.json() + def native_value(self) -> StateType: + if self.get_data() is None: + return None + return self.entity_description.value_fn(self.get_data()) diff --git a/custom_components/antistorm/translations/en.json b/custom_components/antistorm/translations/en.json new file mode 100644 index 0000000..db331eb --- /dev/null +++ b/custom_components/antistorm/translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "description": "Provide a city name.", + "data": { + "city_name": "City name" + } + } + }, + "error": { + "city_not_found": "City not found" + } + }, + "entity": { + "binary_sensor": { + "storm_alarm": { + "name": "Storm Alarm" + }, + "rain_alarm": { + "name": "Rain Alarm" + }, + "storm_active": { + "name": "Storm Active" + } + }, + "sensor": { + "storm_probability": { + "name": "Storm Probability" + }, + "storm_time": { + "name": "Time to Storm" + }, + "precipitation_probability": { + "name": "Precipitation Probability" + }, + "precipitation_time": { + "name": "Time to Precipitation" + } + } + } +} \ No newline at end of file diff --git a/custom_components/antistorm/translations/pl.json b/custom_components/antistorm/translations/pl.json new file mode 100644 index 0000000..7aecf1d --- /dev/null +++ b/custom_components/antistorm/translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "description": "Podaj nazwę miasta.", + "data": { + "city_name": "Nazwa miasta" + } + } + }, + "error": { + "city_not_found": "Podano nieprawidłowe miasto" + } + }, + "entity": { + "binary_sensor": { + "storm_alarm": { + "name": "Alarm Burzowy" + }, + "rain_alarm": { + "name": "Alarm Opadów" + }, + "storm_active": { + "name": "Aktywna Burza" + } + }, + "sensor": { + "storm_probability": { + "name": "Prawdopodobieństwo Burzy" + }, + "storm_time": { + "name": "Czas do Burzy" + }, + "precipitation_probability": { + "name": "Prawdopodobieństwo Opadów" + }, + "precipitation_time": { + "name": "Czas do Opadów" + } + } + } +} \ No newline at end of file diff --git a/custom_components/antistorm/update_coordinator.py b/custom_components/antistorm/update_coordinator.py new file mode 100644 index 0000000..3e3f2e2 --- /dev/null +++ b/custom_components/antistorm/update_coordinator.py @@ -0,0 +1,23 @@ +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .connector import AntistormConnector, AntistormData +from .const import DOMAIN, DEFAULT_UPDATE_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +class AntistormUpdateCoordinator(DataUpdateCoordinator[AntistormData]): + + def __init__(self, hass: HomeAssistant, city_id: int): + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL, + update_method=self.update_method) + self.connector = AntistormConnector(city_id) + + async def update_method(self) -> AntistormData: + return await self.hass.async_add_executor_job(self._update) + + def _update(self) -> AntistormData: + return self.connector.get_data() From 59eef936d86eb0f6b79b7301254a2aadf1b0df72 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Thu, 8 Feb 2024 06:18:54 +0100 Subject: [PATCH 2/6] Update URL --- custom_components/antistorm/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/antistorm/entity.py b/custom_components/antistorm/entity.py index 4528083..f0e66aa 100644 --- a/custom_components/antistorm/entity.py +++ b/custom_components/antistorm/entity.py @@ -7,7 +7,7 @@ from .update_coordinator import AntistormUpdateCoordinator from .connector import AntistormData -from .const import ATTRIBUTION, DEFAULT_NAME, DOMAIN, CONF_CITY_ID, API_URL +from .const import ATTRIBUTION, DEFAULT_NAME, DOMAIN, CONF_CITY_ID, BASE_URL class AntistormEntity(CoordinatorEntity): @@ -37,5 +37,5 @@ def device_info(self) -> DeviceInfo: return { "identifiers": {(DOMAIN, city_id)}, "name": self.base_name(), - "configuration_url": API_URL, + "configuration_url": BASE_URL, } From 1b66deea5fc3a12dc6ff6f3d7138b39bf6653e00 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Thu, 8 Feb 2024 06:40:57 +0100 Subject: [PATCH 3/6] Update hacs.json --- hacs.json | 1 - 1 file changed, 1 deletion(-) diff --git a/hacs.json b/hacs.json index d60f9f5..b4b0e25 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,5 @@ { "name": "Antistorm sensor", - "domains": ["binary_sensor", "sensor"], "country": ["PL"], "render_readme": true, "zip_release": true, From 4718e69809736e9c5c11077f98d467c0874d6769 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Fri, 9 Feb 2024 03:34:14 +0100 Subject: [PATCH 4/6] Imporove config flow --- custom_components/antistorm/config_flow.py | 13 +++++++------ custom_components/antistorm/entity.py | 2 +- custom_components/antistorm/update_coordinator.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/custom_components/antistorm/config_flow.py b/custom_components/antistorm/config_flow.py index 5edcdbb..b85eeb1 100644 --- a/custom_components/antistorm/config_flow.py +++ b/custom_components/antistorm/config_flow.py @@ -12,9 +12,6 @@ _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA_CITY_ID = vol.Schema({ - vol.Required(CONF_CITY_NAME): str, -}) class AntistormFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 @@ -28,14 +25,15 @@ async def get_city_details(self, city_name: str) -> tuple[int, str] | None: async def async_step_user(self, user_input=None) -> FlowResult: errors = {} + usr_city_name = "" if user_input is not None: - usr_city_name = user_input[CONF_CITY_NAME] + usr_city_name = user_input[CONF_CITY_NAME].strip() details = await self.get_city_details(usr_city_name) if details is not None: city_id = details[0] city_name = details[1] return self.async_create_entry( - title= city_name, + title=city_name, data={ CONF_CITY_ID: city_id, CONF_NAME: city_name @@ -43,4 +41,7 @@ async def async_step_user(self, user_input=None) -> FlowResult: ) else: errors[CONF_CITY_NAME] = "city_not_found" - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_CITY_ID, errors=errors) + schema = vol.Schema({ + vol.Required(CONF_CITY_NAME, default=usr_city_name): str, + }) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) diff --git a/custom_components/antistorm/entity.py b/custom_components/antistorm/entity.py index f0e66aa..4cdd8a4 100644 --- a/custom_components/antistorm/entity.py +++ b/custom_components/antistorm/entity.py @@ -12,7 +12,7 @@ class AntistormEntity(CoordinatorEntity): - def __init__(self, coordinator: AntistormUpdateCoordinator, config_entry: ConfigEntry): + def __init__(self, coordinator: AntistormUpdateCoordinator, config_entry: ConfigEntry) -> None: super().__init__(coordinator) self.config_entry = config_entry self.city_id = config_entry.data[CONF_CITY_ID] diff --git a/custom_components/antistorm/update_coordinator.py b/custom_components/antistorm/update_coordinator.py index 3e3f2e2..6e63706 100644 --- a/custom_components/antistorm/update_coordinator.py +++ b/custom_components/antistorm/update_coordinator.py @@ -11,7 +11,7 @@ class AntistormUpdateCoordinator(DataUpdateCoordinator[AntistormData]): - def __init__(self, hass: HomeAssistant, city_id: int): + def __init__(self, hass: HomeAssistant, city_id: int) -> None: super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL, update_method=self.update_method) self.connector = AntistormConnector(city_id) From c486e2cc5b9942f733440b84e1d139268de879c3 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Fri, 9 Feb 2024 03:36:12 +0100 Subject: [PATCH 5/6] Update translations --- custom_components/antistorm/translations/en.json | 2 +- custom_components/antistorm/translations/pl.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/antistorm/translations/en.json b/custom_components/antistorm/translations/en.json index db331eb..9bcb74e 100644 --- a/custom_components/antistorm/translations/en.json +++ b/custom_components/antistorm/translations/en.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Provide a city name.", + "description": "Provide a city name that you want to configure.", "data": { "city_name": "City name" } diff --git a/custom_components/antistorm/translations/pl.json b/custom_components/antistorm/translations/pl.json index 7aecf1d..7758b46 100644 --- a/custom_components/antistorm/translations/pl.json +++ b/custom_components/antistorm/translations/pl.json @@ -2,14 +2,14 @@ "config": { "step": { "user": { - "description": "Podaj nazwę miasta.", + "description": "Podaj nazwę miasta dla którego chcesz skonfigurować integrację.", "data": { "city_name": "Nazwa miasta" } } }, "error": { - "city_not_found": "Podano nieprawidłowe miasto" + "city_not_found": "Nie znaleziono miasta" } }, "entity": { From c4dda254d70ee48c899d396ddfc14c6cf90a12da Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Fri, 9 Feb 2024 03:48:11 +0100 Subject: [PATCH 6/6] Update documentation --- README.md | 70 +++++-------------- custom_components/antistorm/binary_sensor.py | 4 +- .../antistorm/translations/en.json | 4 +- .../antistorm/translations/pl.json | 2 +- hacs.json | 2 +- 5 files changed, 25 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 10289c4..b610b5f 100644 --- a/README.md +++ b/README.md @@ -18,64 +18,38 @@ [downloads_total_shield]: https://img.shields.io/github/downloads/PiotrMachowski/Home-Assistant-custom-components-Antistorm/total -# Antistorm sensor +# Antistorm -This sensor uses official API to get storm warnings from [*Antistorm*](https://antistorm.eu/). For more precise explanation of parameters visit [*Antistorm.eu*](https://antistorm.eu/deweloperzy.php). +This integration returns storm and precipitation warnings from [*Antistorm*](https://antistorm.eu/). -## Configuration options +## Configuration -| Key | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `name` | `string` | `False` | `Antistorm` | Name of sensor | -| `station_id` | `positive int` | `True` | - | ID of monitored station | -| `monitored_conditions` | `list` | `True` | - | List of conditions to monitor | +To configure this integration search for `Antistorm` on *Integrations* page. +Alternatively you can just use the button below: -### Possible monitored conditions +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=antistorm) -#### Binary sensor -| Key | Description | -| --- | --- | -| `storm_alarm` | Status of storm alarm | -| `rain_alarm` | Status of rain alarm | -| `storm_active` | Active storm | +Available binary sensors: + - Storm Alarm + - Precipitation Alarm + - Storm Active -#### Sensor -| Key | Description | -| --- | --- | -| `storm_probability` | Probability of storm | -| `storm_time` | Estimated time until storm | -| `rain_probability` | Probability of rain | -| `rain_time` | Estimated time until rain | +Available sensors: + - Storm Probability + - Time to Storm + - Precipitation Probability + - Time to Precipitation -## Example usage - -``` -binary_sensor: - - platform: antistorm - station_id: 408 - monitored_conditions: - - 'storm_alarm' - - 'rain_alarm' - - 'storm_active' -``` - -``` -sensor: - - platform: antistorm - station_id: 408 - monitored_conditions: - - 'storm_probability' - - 'storm_time' - - 'rain_probability' - - 'rain_time' -``` +For more precise explanation of parameters visit [*Antistorm.eu*](https://antistorm.eu/deweloperzy.php). ## Installation ### Using [HACS](https://hacs.xyz/) (recommended) This integration can be installed using HACS. -To do it search for `Antistorm` in *Integrations* section. +To do it search for `Antistorm` in *Integrations* section, or just use the buton below. + +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=PiotrMachowski&repository=Home-Assistant-custom-components-Antistorm&category=Integration) ### Manual @@ -89,12 +63,6 @@ rm antistorm.zip ``` -## FAQ - -* **How to get value for `station_id`?** - - To find out `station_id` use widget code generator available at page [*Antistorm.eu*](https://antistorm.eu/deweloperzy.php). - diff --git a/custom_components/antistorm/binary_sensor.py b/custom_components/antistorm/binary_sensor.py index 4658c83..f98fc36 100644 --- a/custom_components/antistorm/binary_sensor.py +++ b/custom_components/antistorm/binary_sensor.py @@ -35,8 +35,8 @@ class AntistormBinarySensorEntityDescription(BinarySensorEntityDescription, Anti value_fn=lambda data: data.storm_alarm, ), AntistormBinarySensorEntityDescription( - key='rain_alarm', - translation_key='rain_alarm', + key='precipitation_alarm', + translation_key='precipitation_alarm', icon="mdi:weather-pouring", value_fn=lambda data: data.precipitation_alarm, ), diff --git a/custom_components/antistorm/translations/en.json b/custom_components/antistorm/translations/en.json index 9bcb74e..f491374 100644 --- a/custom_components/antistorm/translations/en.json +++ b/custom_components/antistorm/translations/en.json @@ -17,8 +17,8 @@ "storm_alarm": { "name": "Storm Alarm" }, - "rain_alarm": { - "name": "Rain Alarm" + "precipitation_alarm": { + "name": "Precipitation Alarm" }, "storm_active": { "name": "Storm Active" diff --git a/custom_components/antistorm/translations/pl.json b/custom_components/antistorm/translations/pl.json index 7758b46..78bf100 100644 --- a/custom_components/antistorm/translations/pl.json +++ b/custom_components/antistorm/translations/pl.json @@ -17,7 +17,7 @@ "storm_alarm": { "name": "Alarm Burzowy" }, - "rain_alarm": { + "precipitation_alarm": { "name": "Alarm Opadów" }, "storm_active": { diff --git a/hacs.json b/hacs.json index b4b0e25..9f6cb13 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "Antistorm sensor", + "name": "Antistorm", "country": ["PL"], "render_readme": true, "zip_release": true,