Add configuration flow for Buienradar integration (#37796)

* Add configuration flow for Buienradar integration

* Update buienradar camera tests to work with config flow

* Update buienradar weather tests to work with config flow

* Update buienradar sensor tests to work with config flow

* Remove buienradar config_flow tests to pass tests

* Add config flow tests for buienradar integration

* Increase test coverage for buienradar config_flow tests

* Move data into domain

* Remove forecast option

* Move data to options

* Remove options from config flow

* Adjust tests

* Adjust string

* Fix pylint issues

* Rework review comments

* Handle import

* Change config flow to setup camera or weather

* Fix tests

* Remove translated file

* Fix pylint

* Fix flake8

* Fix unload

* Minor name changes

* Update homeassistant/components/buienradar/config_flow.py

Co-authored-by: Ties de Kock <ties@tiesdekock.nl>

* Remove asynctest

* Add translation

* Disable sensors by default

* Remove integration name from translations

* Remove import method

* Drop  selection between platforms, disable camera by default

* Minor fix in configured_instances

* Bugfix in weather

* Rework import

* Change unique ids of camera

* Fix in import

* Fix camera tests

* Fix sensor test

* Fix sensor test 2

* Fix config flow tests

* Add option flow

* Add tests for option flow

* Add import tests

* Some cleanups

* Apply suggestions from code review

Apply code suggestions

Co-authored-by: Franck Nijhof <git@frenck.dev>

* Fix isort,black,mypy

* Small tweaks and added typing to new parts

* Fix review comments (1)

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix review comments (2)

* Fix issues

* Fix unique id

* Improve tests

* Extend tests

* Fix issue with unload

* Address review comments

* Add warning when loading platform

* Add load/unload test

Co-authored-by: Ties de Kock <ties@tiesdekock.nl>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/50069/head
Rob Bierbooms 2021-05-04 13:49:16 +02:00 committed by GitHub
parent 6931478688
commit c063f14c24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 840 additions and 176 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)),
}
),
)

View File

@ -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)."""

View File

@ -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"
}

View File

@ -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

View File

@ -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"
}
}
}
}
}

View File

@ -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"
}
}
}
}
}

View File

@ -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

View File

@ -37,6 +37,7 @@ FLOWS = [
"broadlink",
"brother",
"bsblan",
"buienradar",
"canary",
"cast",
"cert_expiry",

View File

@ -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=...)`,

View File

@ -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}

View File

@ -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"

View File

@ -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"

View File

@ -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"