diff --git a/CODEOWNERS b/CODEOWNERS index 241b9c8ce5d..f69e9b3ecfe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -76,7 +76,7 @@ homeassistant/components/brother/* @bieniu homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bsblan/* @liudger homeassistant/components/bt_smarthub/* @jxwolstenholme -homeassistant/components/buienradar/* @mjj4791 @ties +homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221 homeassistant/components/cast/* @emontnemery homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/circuit/* @braam diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index 680351f9b81..0474876bf2f 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -1 +1,141 @@ -"""The buienradar component.""" +"""The buienradar integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_COUNTRY, + CONF_DELTA, + CONF_DIMENSION, + CONF_TIMEFRAME, + DEFAULT_COUNTRY, + DEFAULT_DELTA, + DEFAULT_DIMENSION, + DEFAULT_TIMEFRAME, + DOMAIN, +) + +PLATFORMS = ["camera", "sensor", "weather"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the buienradar component.""" + hass.data.setdefault(DOMAIN, {}) + + weather_configs = _filter_domain_configs(config, "weather", DOMAIN) + sensor_configs = _filter_domain_configs(config, "sensor", DOMAIN) + camera_configs = _filter_domain_configs(config, "camera", DOMAIN) + + _import_configs(hass, weather_configs, sensor_configs, camera_configs) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up buienradar from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_update_options)) + 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) + + return unload_ok + + +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +def _import_configs( + hass: HomeAssistant, + weather_configs: list[ConfigType], + sensor_configs: list[ConfigType], + camera_configs: list[ConfigType], +) -> None: + camera_config = {} + if camera_configs: + camera_config = camera_configs[0] + + for config in sensor_configs: + # Remove weather configurations which share lat/lon with sensor configurations + matching_weather_config = None + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + for weather_config in weather_configs: + weather_latitude = config.get(CONF_LATITUDE, hass.config.latitude) + weather_longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + if latitude == weather_latitude and longitude == weather_longitude: + matching_weather_config = weather_config + break + + if matching_weather_config is not None: + weather_configs.remove(matching_weather_config) + + configs = weather_configs + sensor_configs + + if not configs and camera_configs: + config = { + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + } + configs.append(config) + + if configs: + _try_update_unique_id(hass, configs[0], camera_config) + + for config in configs: + data = { + CONF_LATITUDE: config.get(CONF_LATITUDE, hass.config.latitude), + CONF_LONGITUDE: config.get(CONF_LONGITUDE, hass.config.longitude), + CONF_TIMEFRAME: config.get(CONF_TIMEFRAME, DEFAULT_TIMEFRAME), + CONF_COUNTRY: camera_config.get(CONF_COUNTRY, DEFAULT_COUNTRY), + CONF_DELTA: camera_config.get(CONF_DELTA, DEFAULT_DELTA), + CONF_NAME: config.get(CONF_NAME, "Buienradar"), + } + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + ) + + +def _try_update_unique_id( + hass: HomeAssistant, config: ConfigType, camera_config: ConfigType +) -> None: + dimension = camera_config.get(CONF_DIMENSION, DEFAULT_DIMENSION) + country = camera_config.get(CONF_COUNTRY, DEFAULT_COUNTRY) + + registry = entity_registry.async_get(hass) + entity_id = registry.async_get_entity_id("camera", DOMAIN, f"{dimension}_{country}") + + if entity_id is not None: + latitude = config[CONF_LATITUDE] + longitude = config[CONF_LONGITUDE] + + new_unique_id = f"{latitude:2.6f}{longitude:2.6f}" + registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + + +def _filter_domain_configs( + config: ConfigType, domain: str, platform: str +) -> list[ConfigType]: + configs = [] + for entry in config: + if entry.startswith(domain): + configs += [x for x in config[entry] if x["platform"] == platform] + return configs diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 92f25b7ffc6..1a2d6d4d0be 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -9,14 +9,22 @@ import aiohttp import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -CONF_DIMENSION = "dimension" -CONF_DELTA = "delta" -CONF_COUNTRY = "country_code" +from .const import ( + CONF_COUNTRY, + CONF_DELTA, + CONF_DIMENSION, + DEFAULT_COUNTRY, + DEFAULT_DELTA, + DEFAULT_DIMENSION, +) _LOGGER = logging.getLogger(__name__) @@ -41,13 +49,27 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up buienradar radar-loop camera component.""" - dimension = config[CONF_DIMENSION] - delta = config[CONF_DELTA] - name = config[CONF_NAME] - country = config[CONF_COUNTRY] + """Set up buienradar camera platform.""" + _LOGGER.warning( + "Platform configuration is deprecated, will be removed in a future release" + ) - async_add_entities([BuienradarCam(name, dimension, delta, country)]) + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up buienradar radar-loop camera component.""" + config = entry.data + options = entry.options + + country = options.get(CONF_COUNTRY, config.get(CONF_COUNTRY, DEFAULT_COUNTRY)) + + delta = options.get(CONF_DELTA, config.get(CONF_DELTA, DEFAULT_DELTA)) + + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + async_add_entities([BuienradarCam(latitude, longitude, delta, country)]) class BuienradarCam(Camera): @@ -59,7 +81,9 @@ class BuienradarCam(Camera): [0]: https://www.buienradar.nl/overbuienradar/gratis-weerdata """ - def __init__(self, name: str, dimension: int, delta: float, country: str): + def __init__( + self, latitude: float, longitude: float, delta: float, country: str + ) -> None: """ Initialize the component. @@ -67,10 +91,10 @@ class BuienradarCam(Camera): """ super().__init__() - self._name = name + self._name = "Buienradar" # dimension (x and y) of returned radar image - self._dimension = dimension + self._dimension = DEFAULT_DIMENSION # time a cached image stays valid for self._delta = delta @@ -94,7 +118,7 @@ class BuienradarCam(Camera): # deadline for image refresh - self.delta after last successful load self._deadline: datetime | None = None - self._unique_id = f"{self._dimension}_{self._country}" + self._unique_id = f"{latitude:2.6f}{longitude:2.6f}" @property def name(self) -> str: @@ -192,3 +216,8 @@ class BuienradarCam(Camera): def unique_id(self): """Return the unique id.""" return self._unique_id + + @property + def entity_registry_enabled_default(self) -> bool: + """Disable entity by default.""" + return False diff --git a/homeassistant/components/buienradar/config_flow.py b/homeassistant/components/buienradar/config_flow.py new file mode 100644 index 00000000000..e773b39027e --- /dev/null +++ b/homeassistant/components/buienradar/config_flow.py @@ -0,0 +1,129 @@ +"""Config flow for buienradar integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_COUNTRY, + CONF_DELTA, + CONF_TIMEFRAME, + DEFAULT_COUNTRY, + DEFAULT_DELTA, + DEFAULT_TIMEFRAME, + DOMAIN, + SUPPORTED_COUNTRY_CODES, +) + +_LOGGER = logging.getLogger(__name__) + + +class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for buienradar.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> BuienradarOptionFlowHandler: + """Get the options flow for this handler.""" + return BuienradarOptionFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + lat = user_input.get(CONF_LATITUDE) + lon = user_input.get(CONF_LONGITUDE) + + await self.async_set_unique_id(f"{lat}-{lon}") + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=f"{lat},{lon}", data=user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ) + + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors={}, + ) + + async def async_step_import(self, import_input: dict[str, Any]) -> FlowResult: + """Import a config entry.""" + latitude = import_input[CONF_LATITUDE] + longitude = import_input[CONF_LONGITUDE] + + await self.async_set_unique_id(f"{latitude}-{longitude}") + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"{latitude},{longitude}", data=import_input + ) + + +class BuienradarOptionFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_COUNTRY, + default=self.config_entry.options.get( + CONF_COUNTRY, + self.config_entry.data.get(CONF_COUNTRY, DEFAULT_COUNTRY), + ), + ): vol.In(SUPPORTED_COUNTRY_CODES), + vol.Optional( + CONF_DELTA, + default=self.config_entry.options.get( + CONF_DELTA, + self.config_entry.data.get(CONF_DELTA, DEFAULT_DELTA), + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Optional( + CONF_TIMEFRAME, + default=self.config_entry.options.get( + CONF_TIMEFRAME, + self.config_entry.data.get( + CONF_TIMEFRAME, DEFAULT_TIMEFRAME + ), + ), + ): vol.All(vol.Coerce(int), vol.Range(min=5, max=120)), + } + ), + ) diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py index b91d2497d77..cc785512f9b 100644 --- a/homeassistant/components/buienradar/const.py +++ b/homeassistant/components/buienradar/const.py @@ -1,6 +1,24 @@ """Constants for buienradar component.""" + +DOMAIN = "buienradar" + DEFAULT_TIMEFRAME = 60 +DEFAULT_DIMENSION = 700 +DEFAULT_DELTA = 600 + +CONF_DIMENSION = "dimension" +CONF_DELTA = "delta" +CONF_COUNTRY = "country_code" +CONF_TIMEFRAME = "timeframe" + +"""Range according to the docs""" +CAMERA_DIM_MIN = 120 +CAMERA_DIM_MAX = 700 + +SUPPORTED_COUNTRY_CODES = ["NL", "BE"] +DEFAULT_COUNTRY = "NL" + """Schedule next call after (minutes).""" SCHEDULE_OK = 10 """When an error occurred, new call after (minutes).""" diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index bdaa4e166ee..d7759aa9b8d 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -1,8 +1,9 @@ { "domain": "buienradar", "name": "Buienradar", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": ["buienradar==1.0.4"], - "codeowners": ["@mjj4791", "@ties"], + "codeowners": ["@mjj4791", "@ties", "@Robbie1221"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 5ff15a50978..e4a317cface 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -21,6 +21,7 @@ from buienradar.constants import ( import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, @@ -37,11 +38,12 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DEFAULT_TIMEFRAME +from .const import CONF_TIMEFRAME, DEFAULT_TIMEFRAME from .util import BrData _LOGGER = logging.getLogger(__name__) @@ -186,8 +188,6 @@ SENSOR_TYPES = { "symbol_5d": ["Symbol 5d", None, None], } -CONF_TIMEFRAME = "timeframe" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional( @@ -208,14 +208,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up buienradar sensor platform.""" + _LOGGER.warning( + "Platform configuration is deprecated, will be removed in a future release" + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Create the buienradar sensor.""" + config = entry.data + options = entry.options + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - timeframe = config[CONF_TIMEFRAME] + + timeframe = options.get( + CONF_TIMEFRAME, config.get(CONF_TIMEFRAME, DEFAULT_TIMEFRAME) + ) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + return coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} @@ -225,12 +240,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= timeframe, ) - dev = [] - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME), coordinates)) - async_add_entities(dev) + entities = [ + BrSensor(sensor_type, config.get(CONF_NAME, "Buienradar"), coordinates) + for sensor_type in SENSOR_TYPES + ] - data = BrData(hass, coordinates, timeframe, dev) + async_add_entities(entities) + + data = BrData(hass, coordinates, timeframe, entities) # schedule the first update in 1 minute from now: await data.schedule_update(1) @@ -380,7 +397,7 @@ class BrSensor(SensorEntity): self._state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :]) return True - if self.type == WINDSPEED or self.type == WINDGUST: + if self.type in [WINDSPEED, WINDGUST]: # hass wants windspeeds in km/h not m/s, so convert: self._state = data.get(self.type) if self._state is not None: @@ -463,3 +480,8 @@ class BrSensor(SensorEntity): def force_update(self): """Return true for continuous sensors, false for discrete sensors.""" return self._force_update + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False diff --git a/homeassistant/components/buienradar/strings.json b/homeassistant/components/buienradar/strings.json new file mode 100644 index 00000000000..740068a952b --- /dev/null +++ b/homeassistant/components/buienradar/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + } + }, + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Country code of the country to display camera images.", + "delta": "Time interval in seconds between camera image updates", + "timeframe": "Minutes to look ahead for precipitation forecast" + } + } + } + } +} diff --git a/homeassistant/components/buienradar/translations/en.json b/homeassistant/components/buienradar/translations/en.json new file mode 100644 index 00000000000..1965ab05ed9 --- /dev/null +++ b/homeassistant/components/buienradar/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "already_configured": "Location is already configured" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Country code of the country to display camera images.", + "delta": "Time interval in seconds between camera image updates", + "timeframe": "Minutes to look ahead for precipitation forecast" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 2ff638a2550..0aa57efc5f9 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -38,36 +38,42 @@ from homeassistant.components.weather import ( PLATFORM_SCHEMA, WeatherEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback # Reuse data and API logic from the sensor implementation -from .const import DEFAULT_TIMEFRAME +from .const import DEFAULT_TIMEFRAME, DOMAIN from .util import BrData _LOGGER = logging.getLogger(__name__) -DATA_CONDITION = "buienradar_condition" - - CONF_FORECAST = "forecast" +DATA_CONDITION = "buienradar_condition" CONDITION_CLASSES = { - ATTR_CONDITION_CLOUDY: ["c", "p"], - ATTR_CONDITION_FOG: ["d", "n"], - ATTR_CONDITION_HAIL: [], - ATTR_CONDITION_LIGHTNING: ["g"], - ATTR_CONDITION_LIGHTNING_RAINY: ["s"], - ATTR_CONDITION_PARTLYCLOUDY: ["b", "j", "o", "r"], - ATTR_CONDITION_POURING: ["l", "q"], - ATTR_CONDITION_RAINY: ["f", "h", "k", "m"], - ATTR_CONDITION_SNOWY: ["u", "i", "v", "t"], - ATTR_CONDITION_SNOWY_RAINY: ["w"], - ATTR_CONDITION_SUNNY: ["a"], - ATTR_CONDITION_WINDY: [], - ATTR_CONDITION_WINDY_VARIANT: [], - ATTR_CONDITION_EXCEPTIONAL: [], + ATTR_CONDITION_CLOUDY: ("c", "p"), + ATTR_CONDITION_FOG: ("d", "n"), + ATTR_CONDITION_HAIL: (), + ATTR_CONDITION_LIGHTNING: ("g",), + ATTR_CONDITION_LIGHTNING_RAINY: ("s",), + ATTR_CONDITION_PARTLYCLOUDY: ( + "b", + "j", + "o", + "r", + ), + ATTR_CONDITION_POURING: ("l", "q"), + ATTR_CONDITION_RAINY: ("f", "h", "k", "m"), + ATTR_CONDITION_SNOWY: ("u", "i", "v", "t"), + ATTR_CONDITION_SNOWY_RAINY: ("w",), + ATTR_CONDITION_SUNNY: ("a",), + ATTR_CONDITION_WINDY: (), + ATTR_CONDITION_WINDY_VARIANT: (), + ATTR_CONDITION_EXCEPTIONAL: (), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -81,13 +87,24 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up buienradar weather platform.""" + _LOGGER.warning( + "Platform configuration is deprecated, will be removed in a future release" + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the buienradar platform.""" + config = entry.data + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + return coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} @@ -97,12 +114,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates) # create condition helper - if DATA_CONDITION not in hass.data: + if DATA_CONDITION not in hass.data[DOMAIN]: cond_keys = [str(chr(x)) for x in range(97, 123)] - hass.data[DATA_CONDITION] = dict.fromkeys(cond_keys) + hass.data[DOMAIN][DATA_CONDITION] = dict.fromkeys(cond_keys) for cond, condlst in CONDITION_CLASSES.items(): for condi in condlst: - hass.data[DATA_CONDITION][condi] = cond + hass.data[DOMAIN][DATA_CONDITION][condi] = cond async_add_entities([BrWeather(data, config, coordinates)]) @@ -115,8 +132,7 @@ class BrWeather(WeatherEntity): def __init__(self, data, config, coordinates): """Initialise the platform with a data instance and station name.""" - self._stationname = config.get(CONF_NAME) - self._forecast = config[CONF_FORECAST] + self._stationname = config.get(CONF_NAME, "Buienradar") self._data = data self._unique_id = "{:2.6f}{:2.6f}".format( @@ -141,7 +157,7 @@ class BrWeather(WeatherEntity): if self._data and self._data.condition: ccode = self._data.condition.get(CONDCODE) if ccode: - conditions = self.hass.data.get(DATA_CONDITION) + conditions = self.hass.data[DOMAIN].get(DATA_CONDITION) if conditions: return conditions.get(ccode) @@ -187,11 +203,8 @@ class BrWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - if not self._forecast: - return None - fcdata_out = [] - cond = self.hass.data[DATA_CONDITION] + cond = self.hass.data[DOMAIN][DATA_CONDITION] if not self._data.forecast: return None diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3b408860d59..ef577726b99 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -37,6 +37,7 @@ FLOWS = [ "broadlink", "brother", "bsblan", + "buienradar", "canary", "cast", "cert_expiry", diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index c9c6d7b4793..3d0c63d972b 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -1,34 +1,63 @@ """The tests for generic camera component.""" import asyncio from contextlib import suppress +import copy from aiohttp.client_exceptions import ClientResponseError -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR -from homeassistant.setup import async_setup_component +from homeassistant.components.buienradar.const import CONF_COUNTRY, CONF_DELTA, DOMAIN +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + HTTP_INTERNAL_SERVER_ERROR, +) +from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry + # An infinitesimally small time-delta. EPSILON_DELTA = 0.0000000001 +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 -def radar_map_url(dim: int = 512, country_code: str = "NL") -> str: - """Build map url, defaulting to 512 wide (as in component).""" - return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w={dim}&h={dim}" +TEST_CFG_DATA = {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE} + + +def radar_map_url(country_code: str = "NL") -> str: + """Build map URL.""" + return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w=700&h=700" + + +async def _setup_config_entry(hass, entry): + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + domain="camera", + platform="buienradar", + unique_id=f"{TEST_LATITUDE:2.6f}{TEST_LONGITUDE:2.6f}", + config_entry=entry, + original_name="Buienradar", + ) + await hass.async_block_till_done() + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 @@ -38,7 +67,7 @@ async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): # default delta is 600s -> should be the same when calling immediately # afterwards. - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -46,22 +75,19 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): """Test that the cache expires after delta.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "delta": EPSILON_DELTA, - } - }, + options = {CONF_DELTA: EPSILON_DELTA} + + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) - await hass.async_block_till_done() + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 @@ -70,7 +96,7 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): await asyncio.sleep(EPSILON_DELTA) # tiny delta has passed -> should immediately call again - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 @@ -78,15 +104,16 @@ async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): """Test that it fetches with only one request at the same time.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp_1 = client.get("/api/camera_proxy/camera.config_test") - resp_2 = client.get("/api/camera_proxy/camera.config_test") + resp_1 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") + resp_2 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") resp = await resp_1 resp_2 = await resp_2 @@ -96,44 +123,22 @@ async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): assert aioclient_mock.call_count == 1 -async def test_dimension(aioclient_mock, hass, hass_client): - """Test that it actually adheres to the dimension.""" - aioclient_mock.get(radar_map_url(700), text="hello world") - - await async_setup_component( - hass, - "camera", - {"camera": {"name": "config_test", "platform": "buienradar", "dimension": 700}}, - ) - await hass.async_block_till_done() - - client = await hass_client() - - await client.get("/api/camera_proxy/camera.config_test") - - assert aioclient_mock.call_count == 1 - - async def test_belgium_country(aioclient_mock, hass, hass_client): """Test that it actually adheres to another country like Belgium.""" aioclient_mock.get(radar_map_url(country_code="BE"), text="hello world") - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "country_code": "BE", - } - }, - ) - await hass.async_block_till_done() + data = copy.deepcopy(TEST_CFG_DATA) + data[CONF_COUNTRY] = "BE" + + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=data) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -142,15 +147,16 @@ async def test_failure_response_not_cached(aioclient_mock, hass, hass_client): """Test that it does not cache a failure response.""" aioclient_mock.get(radar_map_url(), text="hello world", status=401) - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - await client.get("/api/camera_proxy/camera.config_test") - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 @@ -168,22 +174,19 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): headers={"Last-Modified": last_modified}, ) - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "delta": EPSILON_DELTA, - } - }, + options = {CONF_DELTA: EPSILON_DELTA} + + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) - await hass.async_block_till_done() + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp_1 = await client.get("/api/camera_proxy/camera.config_test") + resp_1 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") # It is not possible to check if header was sent. assert aioclient_mock.call_count == 1 @@ -197,7 +200,7 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): aioclient_mock.get(radar_map_url(), text=None, status=304) - resp_2 = await client.get("/api/camera_proxy/camera.config_test") + resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 assert (await resp_1.read()) == (await resp_2.read()) @@ -205,10 +208,11 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): async def test_retries_after_error(aioclient_mock, hass, hass_client): """Test that it does retry after an error instead of caching.""" - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() @@ -216,7 +220,7 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): # A 404 should not return data and throw: with suppress(ClientResponseError): - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -227,7 +231,7 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): assert aioclient_mock.call_count == 0 # http error should not be cached, immediate retry. - resp_2 = await client.get("/api/camera_proxy/camera.config_test") + resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 # Binary text can not be added as body to `aioclient_mock.get(text=...)`, diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py new file mode 100644 index 00000000000..b8abefec70a --- /dev/null +++ b/tests/components/buienradar/test_config_flow.py @@ -0,0 +1,131 @@ +"""Test the buienradar2 config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry + +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 + + +async def test_config_flow_setup_(hass): + """Test setup of camera.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" + assert result["data"] == { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + +async def test_config_flow_already_configured_weather(hass): + """Test already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=f"{TEST_LATITUDE}-{TEST_LONGITUDE}", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_import_camera(hass): + """Test import of camera.""" + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" + assert result["data"] == { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options_flow(hass): + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"country_code": "BE", "delta": 450, "timeframe": 30}, + ) + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.buienradar.async_unload_entry", return_value=True + ): + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.options == {"country_code": "BE", "delta": 450, "timeframe": 30} diff --git a/tests/components/buienradar/test_init.py b/tests/components/buienradar/test_init.py new file mode 100644 index 00000000000..e3ac8c025e1 --- /dev/null +++ b/tests/components/buienradar/test_init.py @@ -0,0 +1,120 @@ +"""Tests for the buienradar component.""" +from unittest.mock import patch + +from homeassistant.components.buienradar import async_setup +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.entity_registry import async_get_registry + +from tests.common import MockConfigEntry + +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 + + +async def test_import_all(hass): + """Test import of all platforms.""" + config = { + "weather 1": [{"platform": "buienradar", "name": "test1"}], + "sensor 1": [{"platform": "buienradar", "timeframe": 30, "name": "test2"}], + "camera 1": [ + { + "platform": "buienradar", + "country_code": "BE", + "delta": 300, + "name": "test3", + } + ], + } + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + await async_setup(hass, config) + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state == "loaded" + assert entry.data == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "timeframe": 30, + "country_code": "BE", + "delta": 300, + "name": "test2", + } + + +async def test_import_camera(hass): + """Test import of camera platform.""" + entity_registry = await async_get_registry(hass) + entity_registry.async_get_or_create( + domain="camera", + platform="buienradar", + unique_id="512_NL", + original_name="test_name", + ) + await hass.async_block_till_done() + + config = { + "camera 1": [{"platform": "buienradar", "country_code": "NL", "dimension": 512}] + } + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + await async_setup(hass, config) + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state == "loaded" + assert entry.data == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "timeframe": 60, + "country_code": "NL", + "delta": 600, + "name": "Buienradar", + } + + entity_id = entity_registry.async_get_entity_id( + "camera", + "buienradar", + f"{hass.config.latitude:2.6f}{hass.config.longitude:2.6f}", + ) + assert entity_id + entity = entity_registry.async_get(entity_id) + assert entity.original_name == "test_name" + + +async def test_load_unload(hass): + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == "loaded" + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == "not_loaded" diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index 801f5706a08..f0a24b6beb3 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,26 +1,29 @@ """The tests for the Buienradar sensor platform.""" -from homeassistant.components import sensor -from homeassistant.setup import async_setup_component +from unittest.mock import patch + +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry CONDITIONS = ["stationname", "temperature"] -BASE_CONFIG = { - "sensor": [ - { - "platform": "buienradar", - "name": "volkel", - "latitude": 51.65, - "longitude": 5.7, - "monitored_conditions": CONDITIONS, - } - ] -} +TEST_CFG_DATA = {CONF_LATITUDE: 51.5288504, CONF_LONGITUDE: 5.4002156} async def test_smoke_test_setup_component(hass): """Smoke test for successfully set-up with default config.""" - assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.buienradar.sensor.BrSensor.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() for cond in CONDITIONS: - state = hass.states.get(f"sensor.volkel_{cond}") + state = hass.states.get(f"sensor.buienradar_{cond}") assert state.state == "unknown" diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index db0a6ce3984..9d16b531ad0 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -1,25 +1,20 @@ """The tests for the buienradar weather component.""" -from homeassistant.components import weather -from homeassistant.setup import async_setup_component +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -# Example config snippet from documentation. -BASE_CONFIG = { - "weather": [ - { - "platform": "buienradar", - "name": "volkel", - "latitude": 51.65, - "longitude": 5.7, - "forecast": True, - } - ] -} +from tests.common import MockConfigEntry + +TEST_CFG_DATA = {CONF_LATITUDE: 51.5288504, CONF_LONGITUDE: 5.4002156} async def test_smoke_test_setup_component(hass): """Smoke test for successfully set-up with default config.""" - assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG) + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("weather.volkel") + state = hass.states.get("weather.buienradar") assert state.state == "unknown"